package com.example.tracker_app import android.content.ContentValues import android.content.Intent import android.net.Uri import android.os.Build import android.os.Environment import android.provider.MediaStore import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import java.io.IOException class MainActivity : FlutterActivity() { companion object { private const val CHANNEL = "tracker_app/file_exports" // Trailing slash is required by MediaStore on Samsung and some OEMs. private val DOWNLOADS_SUBDIRECTORY = "${Environment.DIRECTORY_DOWNLOADS}/GymDiet/" } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) .setMethodCallHandler { call, result -> when (call.method) { "saveCsvToDownloads" -> saveCsvToDownloads(call, result) "openExportedFile" -> openExportedFile(call, result) else -> result.notImplemented() } } } private fun saveCsvToDownloads(call: MethodCall, result: MethodChannel.Result) { val fileName = call.argument("fileName") val mimeType = call.argument("mimeType") ?: "text/csv" val content = call.argument("content") if (fileName.isNullOrBlank() || content == null) { result.error("invalid_args", "fileName and content are required.", null) return } try { val resolver = applicationContext.contentResolver val collection = MediaStore.Downloads.EXTERNAL_CONTENT_URI val values = ContentValues().apply { put(MediaStore.Downloads.DISPLAY_NAME, fileName) put(MediaStore.Downloads.MIME_TYPE, mimeType) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { put(MediaStore.Downloads.RELATIVE_PATH, DOWNLOADS_SUBDIRECTORY) put(MediaStore.Downloads.IS_PENDING, 1) } } val itemUri = resolver.insert(collection, values) if (itemUri == null) { result.error("save_failed", "Unable to create Downloads entry.", null) return } resolver.openOutputStream(itemUri)?.use { output -> output.write(content.toByteArray(Charsets.UTF_8)) output.flush() } ?: throw IOException("Unable to open output stream.") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val completedValues = ContentValues().apply { put(MediaStore.Downloads.IS_PENDING, 0) } resolver.update(itemUri, completedValues, null, null) } result.success( hashMapOf( "uri" to itemUri.toString(), "path" to "Downloads/GymDiet/$fileName", ), ) } catch (exception: Exception) { result.error("save_failed", exception.message, null) } } private fun openExportedFile(call: MethodCall, result: MethodChannel.Result) { val uriString = call.argument("uri") val mimeType = call.argument("mimeType") ?: "text/csv" if (uriString.isNullOrBlank()) { result.error("invalid_args", "uri is required.", null) return } try { val uri = Uri.parse(uriString) // Try specific MIME type first; fall back to wildcard so Samsung's // "My Files" and other OEM file managers can always handle the intent. val mimeTypes = listOf(mimeType, "*/*") for (mime in mimeTypes) { val viewIntent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(uri, mime) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) // ClipData propagates URI permission through the chooser. clipData = android.content.ClipData.newRawUri("", uri) } val canOpen = packageManager.queryIntentActivities( viewIntent, 0 ).isNotEmpty() if (canOpen) { val chooser = Intent.createChooser(viewIntent, "Open report") chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(chooser) result.success(true) return } } // No app found for either MIME type. result.success(false) } catch (exception: Exception) { result.success(false) } } }