androidandroid-jetpack-composefile-permissionsandroid-mvvm

Android Compose MVVM File Picker Can See File, Cannot Select (Import CSV), permissions, other issues SKD 30+


I've searched everywhere and I can't find any answers that make any sense, and every answer I tried I could find, causes the app to crash instantly. I even got desperate and asked Gemini for help, which... was just only giving me stuff I already know won't work or already tried and stuff that's way deprecated, and I'm absolutely lost. But, I'm also not a developer, I only know the very basics of coding. I tried looking up tutorials on, relevant to this issue but there's none. It's like all the tutorials go from Kindergarten/Elementary School to Doctorate level post-grad with nothing in between.

I don't need anything crazy here. Most of the app is simple, it's a database app for tracking a collection, all offline. This is just an app for me, possibly others for free if they want it, but it's not going on store or anything. My own phone is a Pixel 5 running Android 11, but my main test phone is a Pixel 4a5G running Android 14.

However, I wanted to add a CSV import, because I already have a collection on a different app that I don't like that doesn't work how I want, and it's only options for export are CSV, and I don't want to manually at 135 entries, and if others that collect this same thing want the app too, I'm sure they also would appreciate the option.

So, because CSV is hardly standard, and schemas can be different, I decided to do a simple wizard. The goal is when import CSV is selected, it gets the header and first record and displays the results. From there, you'd choose whether or not the potential header is actually a header (which would then set an option to skip the first row during import) or is a record (include the first row). This also allows seeing that the CSV is being read correctly, and choosing which columns in the CSV map to which values of the Room db (which is one simple table of 7 or 8 values, only two of which are required). I decided to go with the dependency for Apache Commons CSV. I got a CSV helper class setup and the initial import screen...

But right now, I can't even get that far. This file import is the only thing I need to do with files, maybe an export and/or backup (in CSV or the Db files, but that's a ways down the road). I don't even know if my parsing code works, because I can't even select the file to see if it does anything or crashes.

I decided to skip for now the asking of permission (because everything I tried caused crashes) and just grant it manually, but when I can get it to work (on my Android 11 Pixel 5), I can only grand media permissions, not files. And in every case, the file picker works, launches, I can see files, but can't select them. I even checked that my CSV file does have the correct MIME type and everything and does end in .csv.

The current target SDK is 34 and the minimum is 26. It's written in Compose because, well that's the codelabs I took and the examples I had access to in order to learn from. But a modern setup... one activity, app container, screens + view models, separate nav graph. All depencenies are latest versions and the latest version of Android Studio IDE.

TopAppBar has actions, where I put a dropdown menu because there's only going to be 3 options (import, export, and preferences to go to a preferences screen, but maybe not even that because the only preference I can thing to want to be set is force light/dark or use system settings). It is here that I want to implement the import CSV option.

This menu is only visible in the TopAppBar from the HomeScreen, not sure if that will be relevant.

So far, what I've read, you either don't need to explicity grant permission or you need to create some weird get activity call and have to call permissions... and I don't know. Very conflicting answers so, let's just get to where this weird code is that the AI gave me (because all the other samples I tried didn't work either)... and this is as close as I have come, that the file picker opens, I can brows external storage, but cannot select a file. All the code added so far trying to get just this one thing to work (not sure if what I have of the import wizard works yet, because I can't get past selecting a file to test it):

First, I declared in the manifest uses permissions even though it's deprecated:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_*" />
<uses-permission android:name="android.permission.WRITE_MEDIA_*" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

Then... in the App file where the TopAppBar is composed:

@Composable
MyAppBar(...) {
val context = LocalContext.current
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "text/csv"
    }
    val csvHelper = CsvHelper()
    val viewModel: CsvImportViewModel = viewModel()
    val navigateToCsvImportScreen: () -> Unit = {}
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.StartActivityForResult(),
    ) { result ->
            if (result.resultCode == Activity.RESULT_OK) {
                val uri = result.data?.data
                uri?.let {
                    val contentResolver = context.contentResolver
                    contentResolver.openInputStream(uri)?.use { inputStream ->
                        when (val result = csvHelper.csvFileReader(inputStream)) {
                            is CsvResult.Success -> {
                                viewModel.onCsvLoaded(result.header, result.firstRecord)
                                navigateToCsvImportScreen()
                            }
                            is CsvResult.Error -> {
                                /*TODO show error dialog*/
                            }
                            is CsvResult.Empty -> {
                                /*TODO show error dialog*/
                            }
                        }
                    }
                }
            }
        }
    TopAppBar(
        colors = TopAppBarDefaults.topAppBarColors(...

And the TopAppBar Dropdownmenuitem (which, I think navigation call should be here probably and not in the val declared at the top, that's what makes sense to me, but I'm not a developer and that wasn't working either how it intuitively made sense to me):

DropdownMenuItem(
    text = { Text(text = stringResource(R.string.import_csv)) },
    onClick = {
        launcher.launch(intent) // Launch SAF filepicker
        expanded = false
    },

And if it's relevant, CSV helper so far:

class CsvHelper {

    fun csvFileReader(inputStream: InputStream): CsvResult {
        return try {
            val parser = CSVParser.parse(inputStream, Charset.defaultCharset(),
                CSVFormat.DEFAULT)
            val records = parser.records
            if (records.isNotEmpty()) {
                val header = records.first().toList().map { it.toString() }
                val firstRecord = if (records.size > 1) records[1].toList().map { it.toString() }
                else emptyList()
                CsvResult.Success(header, firstRecord)
            } else {
                CsvResult.Empty
            }
        } catch (e: Exception) {
            CsvResult.Error(e)
        }
    }
}

sealed class CsvResult {
    data class Success(val header: List<String>, val firstRecord: List<String>) : CsvResult()
    object Empty : CsvResult()
    data class Error(val exception: Exception) : CsvResult()
}

CsvImportScreen:

@Composable
fun CsvImportBody(
    viewModel: CsvImportViewModel,
    modifier: Modifier = Modifier,
) {
    val csvImportState by viewModel.csvImportState
    val header = csvImportState.header
    val firstRecord = csvImportState.firstRecord

    Column (
        modifier = modifier
            .padding(16.dp)
            .fillMaxWidth()
    ) {
        Text(
            text = stringResource(R.string.csv_import_instructions),
            modifier = modifier
                .padding(bottom = 16.dp),
            softWrap = true
        )
        Column (
            modifier = modifier
                .fillMaxWidth()
        ) {
            Text(
                text = stringResource(R.string.possible_header),
            )
            Text(
                text = "    $header",
            )
        }
        Column (
            modifier = modifier
                .fillMaxWidth()
        ) {
            Text(
                text = stringResource(R.string.possible_record),
            )
            Text(
                text = "    $firstRecord",
            )
        }
    }
}

CsvImportViewModel:

class CsvImportViewModel() : ViewModel() {

    private val _csvImportState = mutableStateOf(CsvImportState())
    val csvImportState: State<CsvImportState> = _csvImportState

    fun onCsvLoaded(header: List<String>, firstRecord: List<String>) {
        _csvImportState.value = CsvImportState(header, firstRecord)
    }
}

data class CsvImportState(
    val header: List<String> = emptyList(),
    val firstRecord: List<String> = emptyList()
)

EDIT

For the time being, I am able to select the files (at least in the emulator running an API 30 Pixel 5). Based on further research leading me to this StackOverflow question/answer and a few others, I have changed the type to "text/*". Though it would be nice if anyone can confirm that for sure, "text/csv" just doesn't work.


Solution

  • You need to set mimetype to text/*