diff --git a/components/browser/engine-gecko-beta/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt b/components/browser/engine-gecko-beta/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt index ae63b646889..9b127921dd5 100644 --- a/components/browser/engine-gecko-beta/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt +++ b/components/browser/engine-gecko-beta/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt @@ -4,10 +4,8 @@ package mozilla.components.browser.engine.gecko.prompt -import android.content.ContentResolver import android.content.Context import android.net.Uri -import android.provider.OpenableColumns import androidx.annotation.VisibleForTesting import mozilla.components.browser.engine.gecko.GeckoEngineSession import mozilla.components.concept.storage.Login @@ -18,7 +16,7 @@ import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.support.base.log.logger.Logger -import mozilla.components.support.ktx.kotlin.sanitizeFileName +import mozilla.components.support.ktx.android.net.getFileName import mozilla.components.support.ktx.kotlin.toDate import org.mozilla.geckoview.AllowOrDeny import org.mozilla.geckoview.GeckoResult @@ -32,8 +30,10 @@ import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.TIM import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.WEEK import org.mozilla.geckoview.GeckoSession.PromptDelegate.PromptResponse import org.mozilla.geckoview.Autocomplete +import java.io.File import java.io.FileOutputStream import java.io.IOException +import java.io.InputStream import java.security.InvalidParameterException import java.text.SimpleDateFormat import java.util.Date @@ -541,9 +541,7 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe val temporalFile = java.io.File(cacheUploadDirectory, getFileName(contentResolver)) try { contentResolver.openInputStream(this)!!.use { inStream -> - FileOutputStream(temporalFile).use { outStream -> - inStream.copyTo(outStream) - } + copyFile(temporalFile, inStream) } } catch (e: IOException) { Logger("GeckoPromptDelegate").warn("Could not convert uri to file uri", e) @@ -551,15 +549,11 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe return Uri.parse("file:///${temporalFile.absolutePath}") } - private fun Uri.getFileName(contentResolver: ContentResolver): String { - val returnUri = this - var fileName = "" - contentResolver.query(returnUri, null, null, null, null)?.use { cursor -> - val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - cursor.moveToFirst() - fileName = cursor.getString(nameIndex) + @VisibleForTesting + internal fun copyFile(temporalFile: File, inStream: InputStream): Long { + return FileOutputStream(temporalFile).use { outStream -> + inStream.copyTo(outStream) } - return fileName.sanitizeFileName() } } diff --git a/components/browser/engine-gecko-beta/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt b/components/browser/engine-gecko-beta/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt index fef20687d16..62c5ba3aeb0 100644 --- a/components/browser/engine-gecko-beta/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt +++ b/components/browser/engine-gecko-beta/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt @@ -13,6 +13,7 @@ import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice import mozilla.components.support.ktx.kotlin.toDate +import mozilla.components.support.test.any import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.whenever @@ -24,6 +25,8 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.Mockito.doReturn import org.mozilla.gecko.util.GeckoBundle import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoSession @@ -35,7 +38,6 @@ import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.WEE import org.mozilla.geckoview.GeckoSession.PromptDelegate.FilePrompt.Capture.ANY import org.mozilla.geckoview.GeckoSession.PromptDelegate.FilePrompt.Capture.NONE import org.mozilla.geckoview.GeckoSession.PromptDelegate.FilePrompt.Capture.USER -import org.robolectric.Shadows.shadowOf import java.io.FileInputStream import java.security.InvalidParameterException import java.util.Calendar @@ -549,20 +551,24 @@ class GeckoPromptDelegateTest { @Test fun `Calling onFilePrompt must provide a FilePicker PromptRequest`() { - val context = testContext - + val context = spy(testContext) + val contentResolver = spy(context.contentResolver) val mockSession = GeckoEngineSession(runtime) var onSingleFileSelectedWasCalled = false var onMultipleFilesSelectedWasCalled = false var onDismissWasCalled = false val mockUri: Uri = mock() - val mockFileInput: FileInputStream = mock() - val shadowContentResolver = shadowOf(context.contentResolver) - shadowContentResolver.registerInputStream(mockUri, mockFileInput) + doReturn(contentResolver).`when`(context).contentResolver + doReturn(mock()).`when`(contentResolver).openInputStream(mozilla.components.support.test.any()) + var filePickerRequest: PromptRequest.File = mock() - val promptDelegate = GeckoPromptDelegate(mockSession) + val promptDelegate = spy(GeckoPromptDelegate(mockSession)) + + // Prevent the file from being copied + doReturn(0L).`when`(promptDelegate).copyFile(any(), any()) + mockSession.register(object : EngineSession.Observer { override fun onPromptRequest(promptRequest: PromptRequest) { filePickerRequest = promptRequest as PromptRequest.File diff --git a/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt b/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt index ae63b646889..9b127921dd5 100644 --- a/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt +++ b/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt @@ -4,10 +4,8 @@ package mozilla.components.browser.engine.gecko.prompt -import android.content.ContentResolver import android.content.Context import android.net.Uri -import android.provider.OpenableColumns import androidx.annotation.VisibleForTesting import mozilla.components.browser.engine.gecko.GeckoEngineSession import mozilla.components.concept.storage.Login @@ -18,7 +16,7 @@ import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.support.base.log.logger.Logger -import mozilla.components.support.ktx.kotlin.sanitizeFileName +import mozilla.components.support.ktx.android.net.getFileName import mozilla.components.support.ktx.kotlin.toDate import org.mozilla.geckoview.AllowOrDeny import org.mozilla.geckoview.GeckoResult @@ -32,8 +30,10 @@ import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.TIM import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.WEEK import org.mozilla.geckoview.GeckoSession.PromptDelegate.PromptResponse import org.mozilla.geckoview.Autocomplete +import java.io.File import java.io.FileOutputStream import java.io.IOException +import java.io.InputStream import java.security.InvalidParameterException import java.text.SimpleDateFormat import java.util.Date @@ -541,9 +541,7 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe val temporalFile = java.io.File(cacheUploadDirectory, getFileName(contentResolver)) try { contentResolver.openInputStream(this)!!.use { inStream -> - FileOutputStream(temporalFile).use { outStream -> - inStream.copyTo(outStream) - } + copyFile(temporalFile, inStream) } } catch (e: IOException) { Logger("GeckoPromptDelegate").warn("Could not convert uri to file uri", e) @@ -551,15 +549,11 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe return Uri.parse("file:///${temporalFile.absolutePath}") } - private fun Uri.getFileName(contentResolver: ContentResolver): String { - val returnUri = this - var fileName = "" - contentResolver.query(returnUri, null, null, null, null)?.use { cursor -> - val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - cursor.moveToFirst() - fileName = cursor.getString(nameIndex) + @VisibleForTesting + internal fun copyFile(temporalFile: File, inStream: InputStream): Long { + return FileOutputStream(temporalFile).use { outStream -> + inStream.copyTo(outStream) } - return fileName.sanitizeFileName() } } diff --git a/components/browser/engine-gecko-nightly/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt b/components/browser/engine-gecko-nightly/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt index fef20687d16..3ce4b1e972c 100644 --- a/components/browser/engine-gecko-nightly/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt +++ b/components/browser/engine-gecko-nightly/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt @@ -13,6 +13,7 @@ import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice import mozilla.components.support.ktx.kotlin.toDate +import mozilla.components.support.test.any import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.whenever @@ -24,6 +25,8 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.Mockito.doReturn import org.mozilla.gecko.util.GeckoBundle import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoSession @@ -35,7 +38,6 @@ import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.WEE import org.mozilla.geckoview.GeckoSession.PromptDelegate.FilePrompt.Capture.ANY import org.mozilla.geckoview.GeckoSession.PromptDelegate.FilePrompt.Capture.NONE import org.mozilla.geckoview.GeckoSession.PromptDelegate.FilePrompt.Capture.USER -import org.robolectric.Shadows.shadowOf import java.io.FileInputStream import java.security.InvalidParameterException import java.util.Calendar @@ -549,20 +551,24 @@ class GeckoPromptDelegateTest { @Test fun `Calling onFilePrompt must provide a FilePicker PromptRequest`() { - val context = testContext - + val context = spy(testContext) + val contentResolver = spy(context.contentResolver) val mockSession = GeckoEngineSession(runtime) var onSingleFileSelectedWasCalled = false var onMultipleFilesSelectedWasCalled = false var onDismissWasCalled = false val mockUri: Uri = mock() - val mockFileInput: FileInputStream = mock() - val shadowContentResolver = shadowOf(context.contentResolver) - shadowContentResolver.registerInputStream(mockUri, mockFileInput) + doReturn(contentResolver).`when`(context).contentResolver + doReturn(mock()).`when`(contentResolver).openInputStream(any()) + var filePickerRequest: PromptRequest.File = mock() - val promptDelegate = GeckoPromptDelegate(mockSession) + val promptDelegate = spy(GeckoPromptDelegate(mockSession)) + + // Prevent the file from being copied + doReturn(0L).`when`(promptDelegate).copyFile(any(), any()) + mockSession.register(object : EngineSession.Observer { override fun onPromptRequest(promptRequest: PromptRequest) { filePickerRequest = promptRequest as PromptRequest.File diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt index ae63b646889..9b127921dd5 100644 --- a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt +++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt @@ -4,10 +4,8 @@ package mozilla.components.browser.engine.gecko.prompt -import android.content.ContentResolver import android.content.Context import android.net.Uri -import android.provider.OpenableColumns import androidx.annotation.VisibleForTesting import mozilla.components.browser.engine.gecko.GeckoEngineSession import mozilla.components.concept.storage.Login @@ -18,7 +16,7 @@ import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.support.base.log.logger.Logger -import mozilla.components.support.ktx.kotlin.sanitizeFileName +import mozilla.components.support.ktx.android.net.getFileName import mozilla.components.support.ktx.kotlin.toDate import org.mozilla.geckoview.AllowOrDeny import org.mozilla.geckoview.GeckoResult @@ -32,8 +30,10 @@ import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.TIM import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.WEEK import org.mozilla.geckoview.GeckoSession.PromptDelegate.PromptResponse import org.mozilla.geckoview.Autocomplete +import java.io.File import java.io.FileOutputStream import java.io.IOException +import java.io.InputStream import java.security.InvalidParameterException import java.text.SimpleDateFormat import java.util.Date @@ -541,9 +541,7 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe val temporalFile = java.io.File(cacheUploadDirectory, getFileName(contentResolver)) try { contentResolver.openInputStream(this)!!.use { inStream -> - FileOutputStream(temporalFile).use { outStream -> - inStream.copyTo(outStream) - } + copyFile(temporalFile, inStream) } } catch (e: IOException) { Logger("GeckoPromptDelegate").warn("Could not convert uri to file uri", e) @@ -551,15 +549,11 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe return Uri.parse("file:///${temporalFile.absolutePath}") } - private fun Uri.getFileName(contentResolver: ContentResolver): String { - val returnUri = this - var fileName = "" - contentResolver.query(returnUri, null, null, null, null)?.use { cursor -> - val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - cursor.moveToFirst() - fileName = cursor.getString(nameIndex) + @VisibleForTesting + internal fun copyFile(temporalFile: File, inStream: InputStream): Long { + return FileOutputStream(temporalFile).use { outStream -> + inStream.copyTo(outStream) } - return fileName.sanitizeFileName() } } diff --git a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt index fef20687d16..fa033d40e36 100644 --- a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt +++ b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt @@ -13,6 +13,7 @@ import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice import mozilla.components.support.ktx.kotlin.toDate +import mozilla.components.support.test.any import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.whenever @@ -24,6 +25,8 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.Mockito.doReturn import org.mozilla.gecko.util.GeckoBundle import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoSession @@ -35,7 +38,6 @@ import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.WEE import org.mozilla.geckoview.GeckoSession.PromptDelegate.FilePrompt.Capture.ANY import org.mozilla.geckoview.GeckoSession.PromptDelegate.FilePrompt.Capture.NONE import org.mozilla.geckoview.GeckoSession.PromptDelegate.FilePrompt.Capture.USER -import org.robolectric.Shadows.shadowOf import java.io.FileInputStream import java.security.InvalidParameterException import java.util.Calendar @@ -549,20 +551,23 @@ class GeckoPromptDelegateTest { @Test fun `Calling onFilePrompt must provide a FilePicker PromptRequest`() { - val context = testContext - + val context = spy(testContext) + val contentResolver = spy(context.contentResolver) val mockSession = GeckoEngineSession(runtime) var onSingleFileSelectedWasCalled = false var onMultipleFilesSelectedWasCalled = false var onDismissWasCalled = false val mockUri: Uri = mock() - val mockFileInput: FileInputStream = mock() - val shadowContentResolver = shadowOf(context.contentResolver) - shadowContentResolver.registerInputStream(mockUri, mockFileInput) + doReturn(contentResolver).`when`(context).contentResolver + doReturn(mock()).`when`(contentResolver).openInputStream(any()) var filePickerRequest: PromptRequest.File = mock() - val promptDelegate = GeckoPromptDelegate(mockSession) + val promptDelegate = spy(GeckoPromptDelegate(mockSession)) + + // Prevent the file from being copied + doReturn(0L).`when`(promptDelegate).copyFile(any(), any()) + mockSession.register(object : EngineSession.Observer { override fun onPromptRequest(promptRequest: PromptRequest) { filePickerRequest = promptRequest as PromptRequest.File diff --git a/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt b/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt index cf9659c9042..3970e62e25a 100644 --- a/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt +++ b/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt @@ -8,8 +8,13 @@ import android.content.ContentResolver import android.content.Context import android.net.Uri import android.os.Build +import android.provider.OpenableColumns +import android.webkit.MimeTypeMap +import androidx.annotation.VisibleForTesting +import mozilla.components.support.ktx.kotlin.sanitizeFileName import java.io.File import java.io.IOException +import java.util.UUID private val commonPrefixes = listOf("www.", "mobile.", "m.") @@ -85,3 +90,55 @@ fun Uri.isUnderPrivateAppDirectory(context: Context): Boolean { else -> false } } + +/** + * Return a file name for [this] give Uri. + * @return A file name for the content, or generated file name if the URL is invalid or the type is unknown + */ +fun Uri.getFileName(contentResolver: ContentResolver): String { + return when (this.scheme) { + ContentResolver.SCHEME_FILE -> File(path ?: "").name.sanitizeFileName() + ContentResolver.SCHEME_CONTENT -> getFileNameForContentUris(contentResolver) + else -> { + generateFileName(getFileExtension(contentResolver)) + } + } +} + +/** + * Return a file extension for [this] give Uri (only supports content:// schemes). + * @return A file extension for the content, or empty string if the URL is invalid or the type is unknown + */ +fun Uri.getFileExtension(contentResolver: ContentResolver): String { + return MimeTypeMap.getSingleton().getExtensionFromMimeType(contentResolver.getType(this)) ?: "" +} + +@VisibleForTesting +internal fun Uri.getFileNameForContentUris(contentResolver: ContentResolver): String { + var fileName = "" + contentResolver.query(this, null, null, null, null)?.use { cursor -> + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + val fileExtension = getFileExtension(contentResolver) + fileName = if (nameIndex == -1) { + generateFileName(fileExtension) + } else { + cursor.moveToFirst() + cursor.getString(nameIndex) ?: generateFileName(fileExtension) + } + } + return fileName.sanitizeFileName() +} + +/** + * Generate a file name using a randomUUID + the current timestamp. + */ +@VisibleForTesting +internal fun generateFileName(fileExtension: String = ""): String { + val randomId = UUID.randomUUID().toString().removePrefix("-").trim() + val timeStamp = System.currentTimeMillis() + return if (fileExtension.isNotEmpty()) { + "$randomId$timeStamp.$fileExtension" + } else { + "$randomId$timeStamp" + } +} diff --git a/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt b/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt index db4b6dc442e..d10d2e762cc 100644 --- a/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt +++ b/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt @@ -4,14 +4,22 @@ package mozilla.components.support.ktx.android.net +import android.content.ContentResolver +import android.database.Cursor +import android.webkit.MimeTypeMap import androidx.core.net.toUri import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock 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.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.any +import org.mockito.Mockito.doReturn +import org.robolectric.Shadows @RunWith(AndroidJUnit4::class) class UriTest { @@ -109,4 +117,122 @@ class UriTest { assertTrue("https://foo.bar:443/bobo".toUri().sameOriginAs("https://foo.bar:443/obob".toUri())) assertTrue("https://foo.bar:333".toUri().sameOriginAs("https://foo.bar:333".toUri())) } + + @Test + fun testGenerateFileName() { + val fileExtension = "txt" + var fileName = generateFileName(fileExtension) + + assertTrue(fileName.contains(fileExtension)) + + fileName = generateFileName() + + assertFalse(fileName.contains(".")) + } + + @Test + fun testGetFileExtension() { + val resolver = mock() + val uri = "content://media/external/file/37162".toUri() + + Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("txt", "text/plain") + + doReturn("text/plain").`when`(resolver).getType(any()) + + assertEquals("txt", uri.getFileExtension(resolver)) + } + + @Test + fun `getFileNameForContentUris for urls with DISPLAY_NAME`() { + val resolver = mock() + val uri = "content://media/external/file/37162".toUri() + val cursor = mock() + + Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("txt", "text/plain") + doReturn("text/plain").`when`(resolver).getType(any()) + + doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any()) + doReturn(1).`when`(cursor).getColumnIndex(any()) + doReturn("myFile.txt").`when`(cursor).getString(anyInt()) + + assertEquals("myFile.txt", uri.getFileNameForContentUris(resolver)) + } + + @Test + fun `getFileNameForContentUris for urls without DISPLAY_NAME`() { + val resolver = mock() + val uri = "content://media/external/file/37162".toUri() + val cursor = mock() + + Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("txt", "text/plain") + doReturn("text/plain").`when`(resolver).getType(any()) + + doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any()) + doReturn(-1).`when`(cursor).getColumnIndex(any()) + + val fileName = uri.getFileNameForContentUris(resolver) + + assertTrue(fileName.contains(".txt")) + assertTrue(fileName.isNotEmpty()) + } + + @Test + fun `getFileNameForContentUris for urls with null DISPLAY_NAME`() { + val resolver = mock() + val uri = "content://media/external/file/37162".toUri() + val cursor = mock() + + Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("txt", "text/plain") + doReturn("text/plain").`when`(resolver).getType(any()) + + doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any()) + doReturn(1).`when`(cursor).getColumnIndex(any()) + doReturn(null).`when`(cursor).getString(anyInt()) + + val fileName = uri.getFileNameForContentUris(resolver) + + assertTrue(fileName.contains(".txt")) + assertTrue(fileName.isNotEmpty()) + } + + @Test + fun `getFileName for file uri schemes`() { + val resolver = mock() + val uri = "file:///home/user/myfile.html".toUri() + + assertEquals("myfile.html", uri.getFileName(resolver)) + } + + @Test + fun `getFileName for content uri schemes`() { + val resolver = mock() + val uri = "content://media/external/file/37162".toUri() + val cursor = mock() + + Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("txt", "text/plain") + doReturn("text/plain").`when`(resolver).getType(any()) + + doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any()) + doReturn(1).`when`(cursor).getColumnIndex(any()) + doReturn(null).`when`(cursor).getString(anyInt()) + + val fileName = uri.getFileName(resolver) + + assertTrue(fileName.contains(".txt")) + assertTrue(fileName.isNotEmpty()) + } + + @Test + fun `getFileName for UNKNOWN uri schemes will generate file name`() { + val resolver = mock() + val uri = "UNKNOWN://media/external/file/37162".toUri() + + Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("txt", "text/plain") + doReturn("text/plain").`when`(resolver).getType(any()) + + val fileName = uri.getFileName(resolver) + + assertTrue(fileName.contains(".txt")) + assertTrue(fileName.isNotEmpty()) + } } diff --git a/docs/changelog.md b/docs/changelog.md index b0fe02a5519..1c46cb2410b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,6 +12,8 @@ permalink: /changelog/ * [Gecko](https://github.com/mozilla-mobile/android-components/blob/master/buildSrc/src/main/java/Gecko.kt) * [Configuration](https://github.com/mozilla-mobile/android-components/blob/master/buildSrc/src/main/java/Config.kt) +* **browser-engine-gecko**, **browser-engine-gecko-beta**, **browser-engine-gecko-nightly** + * Fixed issue [#7983](https://github.com/mozilla-mobile/android-components/issues/7983), crash when a file name wasn't provided when uploading a file. # 54.0.0