Skip to content

Commit 63a7a4b

Browse files
committed
Improved UriExt and added tests
1 parent ef62803 commit 63a7a4b

File tree

6 files changed

+130
-29
lines changed

6 files changed

+130
-29
lines changed
Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package org.odk.collect.androidshared.system
22

33
import android.content.ContentResolver
4+
import android.content.Context
45
import android.net.Uri
56
import android.provider.OpenableColumns
7+
import android.webkit.MimeTypeMap
8+
import androidx.core.net.toFile
69
import java.io.File
710
import java.io.FileOutputStream
811

9-
fun Uri.copyToFile(contentResolver: ContentResolver, dest: File) {
12+
fun Uri.copyToFile(context: Context, dest: File) {
1013
try {
11-
contentResolver.openInputStream(this)?.use { inputStream ->
14+
context.contentResolver.openInputStream(this)?.use { inputStream ->
1215
FileOutputStream(dest).use { outputStream ->
1316
inputStream.copyTo(outputStream)
1417
}
@@ -18,19 +21,73 @@ fun Uri.copyToFile(contentResolver: ContentResolver, dest: File) {
1821
}
1922
}
2023

21-
fun Uri.getFileName(contentResolver: ContentResolver): String? {
24+
fun Uri.getFileExtension(context: Context): String? {
25+
var extension = getFileName(context)?.substringAfterLast(".", "")
26+
27+
if (extension.isNullOrEmpty()) {
28+
val mimeType = context.contentResolver.getType(this)
29+
30+
extension = if (scheme == ContentResolver.SCHEME_CONTENT) {
31+
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
32+
} else {
33+
MimeTypeMap.getFileExtensionFromUrl(toString())
34+
}
35+
36+
if (extension.isNullOrEmpty()) {
37+
extension = mimeType?.substringAfterLast("/", "")
38+
}
39+
}
40+
41+
return if (extension.isNullOrEmpty()) {
42+
null
43+
} else {
44+
".$extension"
45+
}
46+
}
47+
48+
fun Uri.getFileName(context: Context): String? {
2249
var fileName: String? = null
23-
if (scheme == ContentResolver.SCHEME_CONTENT) {
24-
val cursor = contentResolver.query(this, null, null, null, null)
25-
cursor.use {
26-
if (it != null && it.moveToFirst()) {
27-
val fileNameColumnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
28-
fileName = it.getString(fileNameColumnIndex)
50+
51+
try {
52+
when (scheme) {
53+
ContentResolver.SCHEME_FILE -> fileName = toFile().name
54+
ContentResolver.SCHEME_CONTENT -> {
55+
val cursor = context.contentResolver.query(this, null, null, null, null)
56+
cursor?.use {
57+
if (it.moveToFirst()) {
58+
val fileNameColumnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
59+
if (fileNameColumnIndex != -1) {
60+
fileName = it.getString(fileNameColumnIndex)
61+
}
62+
}
63+
}
64+
}
65+
ContentResolver.SCHEME_ANDROID_RESOURCE -> {
66+
// for uris like [android.resource://com.example.app/1234567890]
67+
val resourceId = lastPathSegment?.toIntOrNull()
68+
if (resourceId != null) {
69+
fileName = context.resources.getResourceName(resourceId)
70+
} else {
71+
// for uris like [android.resource://com.example.app/raw/sample]
72+
val packageName = authority
73+
if (pathSegments.size >= 2) {
74+
val resourceType = pathSegments[0]
75+
val resourceEntryName = pathSegments[1]
76+
val resId = context.resources.getIdentifier(resourceEntryName, resourceType, packageName)
77+
if (resId != 0) {
78+
fileName = context.resources.getResourceName(resId)
79+
}
80+
}
81+
}
2982
}
3083
}
84+
85+
if (fileName == null) {
86+
fileName = path?.substringAfterLast("/")
87+
}
88+
} catch (e: Exception) {
89+
// ignore
3190
}
32-
if (fileName == null) {
33-
fileName = path?.substringAfterLast("/")
34-
}
91+
3592
return fileName
3693
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package org.odk.collect.androidshared.system
2+
3+
import android.app.Application
4+
import androidx.core.net.toUri
5+
import androidx.test.core.app.ApplicationProvider
6+
import androidx.test.ext.junit.runners.AndroidJUnit4
7+
import org.hamcrest.CoreMatchers.equalTo
8+
import org.hamcrest.MatcherAssert.assertThat
9+
import org.junit.Test
10+
import org.junit.runner.RunWith
11+
import org.odk.collect.shared.TempFiles
12+
13+
@RunWith(AndroidJUnit4::class)
14+
class UriExtTest {
15+
private val context = ApplicationProvider.getApplicationContext<Application>()
16+
17+
@Test
18+
fun `copyToFile copies the source file to the target file`() {
19+
val sourceFile = TempFiles.createTempFile().also {
20+
it.writeText("blah")
21+
}
22+
val sourceFileUri = sourceFile.toUri()
23+
val targetFile = TempFiles.createTempFile()
24+
25+
sourceFileUri.copyToFile(context, targetFile)
26+
assertThat(targetFile.readText(), equalTo(sourceFile.readText()))
27+
}
28+
29+
@Test
30+
fun `getFileExtension returns file extension`() {
31+
val file = TempFiles.createTempFile(".jpg")
32+
val fileUri = file.toUri()
33+
34+
assertThat(fileUri.getFileExtension(context), equalTo(".jpg"))
35+
}
36+
37+
@Test
38+
fun `getFileName returns file name`() {
39+
val file = TempFiles.createTempFile()
40+
val fileUri = file.toUri()
41+
42+
assertThat(fileUri.getFileName(context), equalTo(file.name))
43+
}
44+
}

maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class OfflineMapLayersImporter(
2121
val viewModel: OfflineMapLayersViewModel by activityViewModels {
2222
object : ViewModelProvider.Factory {
2323
override fun <T : ViewModel> create(modelClass: Class<T>): T {
24-
return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider, requireContext().contentResolver) as T
24+
return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider) as T
2525
}
2626
}
2727
}

maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class OfflineMapLayersPicker(
3030
private val viewModel: OfflineMapLayersViewModel by activityViewModels {
3131
object : ViewModelProvider.Factory {
3232
override fun <T : ViewModel> create(modelClass: Class<T>): T {
33-
return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider, requireContext().contentResolver) as T
33+
return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider) as T
3434
}
3535
}
3636
}
@@ -39,7 +39,7 @@ class OfflineMapLayersPicker(
3939

4040
private val getLayers = registerForActivityResult(ActivityResultContracts.GetMultipleContents(), registry) { uris ->
4141
if (uris.isNotEmpty()) {
42-
viewModel.loadLayersToImport(uris)
42+
viewModel.loadLayersToImport(uris, requireContext())
4343
DialogFragmentUtils.showIfNotShowing(
4444
OfflineMapLayersImporter::class.java,
4545
childFragmentManager

maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package org.odk.collect.maps.layers
22

3-
import android.content.ContentResolver
3+
import android.content.Context
44
import android.net.Uri
55
import androidx.lifecycle.LiveData
66
import androidx.lifecycle.MutableLiveData
77
import androidx.lifecycle.ViewModel
88
import org.odk.collect.androidshared.system.copyToFile
9+
import org.odk.collect.androidshared.system.getFileExtension
910
import org.odk.collect.androidshared.system.getFileName
1011
import org.odk.collect.async.Scheduler
1112
import org.odk.collect.settings.SettingsProvider
@@ -16,8 +17,7 @@ import java.io.File
1617
class OfflineMapLayersViewModel(
1718
private val referenceLayerRepository: ReferenceLayerRepository,
1819
private val scheduler: Scheduler,
19-
private val settingsProvider: SettingsProvider,
20-
private val contentResolver: ContentResolver
20+
private val settingsProvider: SettingsProvider
2121
) : ViewModel() {
2222
private val _isLoading = MutableLiveData<Boolean>()
2323
val isLoading: LiveData<Boolean> = _isLoading
@@ -49,7 +49,7 @@ class OfflineMapLayersViewModel(
4949
)
5050
}
5151

52-
fun loadLayersToImport(uris: List<Uri>) {
52+
fun loadLayersToImport(uris: List<Uri>, context: Context) {
5353
_isLoading.value = true
5454
scheduler.immediate(
5555
background = {
@@ -58,10 +58,10 @@ class OfflineMapLayersViewModel(
5858
}
5959
val layers = mutableListOf<ReferenceLayer>()
6060
uris.forEach { uri ->
61-
uri.getFileName(contentResolver)?.let { fileName ->
62-
if (fileName.endsWith(MbtilesFile.FILE_EXTENSION)) {
61+
if (uri.getFileExtension(context) == MbtilesFile.FILE_EXTENSION) {
62+
uri.getFileName(context)?.let { fileName ->
6363
val layerFile = File(tempLayersDir, fileName).also { file ->
64-
uri.copyToFile(contentResolver, file)
64+
uri.copyToFile(context, file)
6565
}
6666
layers.add(ReferenceLayer(layerFile.absolutePath, layerFile, MbtilesFile.readName(layerFile) ?: layerFile.name))
6767
}

maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class OfflineMapLayersImporterTest {
6363
launchFragment().onFragment {
6464
scheduler.flush()
6565
assertThat(it.isVisible, equalTo(true))
66-
it.viewModel.loadLayersToImport(emptyList())
66+
it.viewModel.loadLayersToImport(emptyList(), it.requireContext())
6767
onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click())
6868
scheduler.flush()
6969
RobolectricHelpers.runLooper()
@@ -77,7 +77,7 @@ class OfflineMapLayersImporterTest {
7777
val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION)
7878

7979
launchFragment().onFragment {
80-
it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()))
80+
it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext())
8181
}
8282

8383
onView(withId(org.odk.collect.maps.R.id.progress_indicator)).check(matches(isDisplayed()))
@@ -150,7 +150,7 @@ class OfflineMapLayersImporterTest {
150150
val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION)
151151

152152
launchFragment().onFragment {
153-
it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()))
153+
it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext())
154154
}
155155

156156
scheduler.flush()
@@ -166,7 +166,7 @@ class OfflineMapLayersImporterTest {
166166
val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION)
167167

168168
val scenario = launchFragment().onFragment {
169-
it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()))
169+
it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext())
170170
}
171171

172172
scheduler.flush()
@@ -184,7 +184,7 @@ class OfflineMapLayersImporterTest {
184184
val file2 = TempFiles.createTempFile("layer2", ".txt")
185185

186186
launchFragment().onFragment {
187-
it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()))
187+
it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext())
188188
}
189189

190190
scheduler.flush()
@@ -200,7 +200,7 @@ class OfflineMapLayersImporterTest {
200200
val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION)
201201

202202
launchFragment().onFragment {
203-
it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()))
203+
it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext())
204204
}
205205

206206
scheduler.flush()
@@ -224,7 +224,7 @@ class OfflineMapLayersImporterTest {
224224
val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION)
225225

226226
launchFragment().onFragment {
227-
it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()))
227+
it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext())
228228
}
229229

230230
scheduler.flush()

0 commit comments

Comments
 (0)