From da54acd7d4b3c15eb854971ac7820704d4b8eb42 Mon Sep 17 00:00:00 2001 From: Alan Trope Date: Wed, 13 Mar 2024 23:38:14 -0300 Subject: [PATCH] chore: 1st test --- android/build.gradle | 5 +- .../media_store_plus/FileOperationWorker.kt | 225 ++++++++++++++++++ .../media_store_plus/MediaStorePlusPlugin.kt | 118 ++++----- 3 files changed, 271 insertions(+), 77 deletions(-) create mode 100644 android/src/main/kotlin/com/snnafi/media_store_plus/FileOperationWorker.kt diff --git a/android/build.gradle b/android/build.gradle index 976aefc..ee1cafc 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -47,7 +47,8 @@ android { dependencies { implementation("androidx.documentfile:documentfile:1.0.1") - implementation("com.google.code.gson:gson:2.9.0") + implementation("com.google.code.gson:gson:2.10") implementation 'com.mpatric:mp3agic:0.9.1' -// implementation files('/Users/suamusica/flutter/bin/cache/artifacts/engine/android-x64/flutter.jar') +// implementation files('/Users/alantrope/flutter/bin/cache/artifacts/engine/android-x64/flutter.jar') + implementation "androidx.work:work-runtime:2.9.0" } diff --git a/android/src/main/kotlin/com/snnafi/media_store_plus/FileOperationWorker.kt b/android/src/main/kotlin/com/snnafi/media_store_plus/FileOperationWorker.kt new file mode 100644 index 0000000..712db41 --- /dev/null +++ b/android/src/main/kotlin/com/snnafi/media_store_plus/FileOperationWorker.kt @@ -0,0 +1,225 @@ +package com.snnafi.media_store_plus + +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import android.util.Log +import androidx.annotation.RequiresApi +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.mpatric.mp3agic.ID3v24Tag +import com.mpatric.mp3agic.Mp3File +import kotlinx.coroutines.delay +import java.io.File + +class FileOperationWorker( + context: Context, + workerParams: WorkerParameters +) : CoroutineWorker(context, workerParams) { + + override suspend fun doWork(): Result { + try { + // Example of file operation, replace with actual work + val path = inputData.getString("path") ?: return Result.failure() + val name = inputData.getString("name") ?: return Result.failure() + val appFolder = inputData.getString("appFolder") ?: return Result.failure() + val dirType = inputData.getInt("dirType",0) ?: return Result.failure() + val dirName = inputData.getString("dirName") ?: return Result.failure() + val externalVolumeName = inputData.getString("externalVolumeName") + val id3v2TagsJson = inputData.getString("id3v2Tags") + + val gson = Gson() + val type = object : TypeToken>() {}.type + val id3v2Tags: Map = gson.fromJson(id3v2TagsJson, type) + + Log.d(TAG, "Processing file $name") + + saveId3(path, id3v2Tags) + val relativePath: String = if (appFolder.trim().isEmpty()) { + dirName + } else { + dirName + File.separator + appFolder + } + + deleteFileUsingDisplayName( + applicationContext, + name, + appFolder, + dirType, + dirName, + externalVolumeName + ) + Log.d(TAG, "file $name after delete") + + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val values = ContentValues().apply { + put(MediaStore.Audio.Media.DISPLAY_NAME, name) + put(MediaStore.Audio.Media.RELATIVE_PATH, relativePath) + put(MediaStore.Audio.Media.IS_PENDING, 1) + } + + + val resolver = applicationContext.contentResolver + val uri = resolver.insert( + getUriFromDirType(applicationContext,dirType, externalVolumeName), + values + )!! + + resolver.openOutputStream(uri).use { os -> + File(path).inputStream().use { it.copyTo(os!!) } + } + + values.clear() + values.put(MediaStore.Audio.Media.IS_PENDING, 0) + resolver.update(uri, values, null, null) + + Log.d(TAG, "saveFile $name") + + } + + Log.d(TAG, "File $name processed") + if (File(path).exists()) { + File(path).delete() + } + // Successfully finished + return Result.success() + } catch (e: Exception) { + // Handle failure + Log.e(TAG, "Error processing file", e) + return Result.failure() + } + } + + companion object { + const val TAG = "FileOperationWorker" + } + + private fun getUriFromDisplayName( + ctx: Context, + + displayName: String, + appFolder: String, + dirType: Int, + dirName: String, + externalVolumeName: String?, + ): Uri? { + + val uri = getUriFromDirType(ctx,dirType, externalVolumeName) + + val relativePath: String = if (appFolder.trim().isEmpty()) { + dirName + File.separator + } else { + dirName + File.separator + appFolder + File.separator + } + + val projection: Array = arrayOf(MediaStore.MediaColumns._ID) + val selectionArgs = + arrayOf(displayName, relativePath) + val cursor: Cursor = ctx.contentResolver.query( + uri, + projection, + MediaStore.Audio.Media.DISPLAY_NAME + " =? AND " + MediaStore.Audio.Media.RELATIVE_PATH + " =? ", + selectionArgs, + null + )!! + cursor.moveToFirst() + Log.d(TAG, "getUriFromDisplayName: $uri") + return if (cursor.count > 0) { + val columnIndex: Int = cursor.getColumnIndex(projection[0]) + val fileId: Long = cursor.getLong(columnIndex) + cursor.close() + Log.d(TAG, "getUriFromDisplayName2: $uri/$fileId") + Uri.parse("$uri/$fileId") + } else { + null + } + + } + private fun deleteFileUsingDisplayName( + ctx: Context, + displayName: String, + appFolder: String, + dirType: Int, + dirName: String, + externalVolumeName: String?, + ): Boolean { + val relativePath: String = if (appFolder.trim().isEmpty()) { + dirName + File.separator + } else { + dirName + File.separator + appFolder + File.separator + } + val uri: Uri? = + getUriFromDisplayName(ctx,displayName, appFolder, dirType, dirName, externalVolumeName) + Log.d(TAG, "deleteFileUsingDisplayName DisplayName: $displayName URI:$uri") + if (uri != null) { + val selectionArgs = + arrayOf(displayName, relativePath) + ctx.contentResolver.delete( + uri, + MediaStore.Audio.Media.DISPLAY_NAME + " =? AND " + MediaStore.Audio.Media.RELATIVE_PATH + " =? ", + selectionArgs + ) + Log.d("deleteFile", displayName) + return true + } + return false + } + @RequiresApi(Build.VERSION_CODES.Q) + private fun defineVolume(ctx: Context, externalVolumeName: String?): String { + return if (externalVolumeName != null) { + MediaStore.getExternalVolumeNames(ctx) + .find { it.lowercase() == externalVolumeName.lowercase() } + ?: MediaStore.VOLUME_EXTERNAL_PRIMARY + } else { + MediaStore.VOLUME_EXTERNAL_PRIMARY + } + } + + private fun getUriFromDirType(ctx: Context,dirType: Int, externalVolumeName: String?): Uri { + return when (dirType) { + 0 -> MediaStore.Images.Media.getContentUri(defineVolume(ctx,externalVolumeName)) + 1 -> MediaStore.Audio.Media.getContentUri(defineVolume(ctx,externalVolumeName)) + 2 -> MediaStore.Video.Media.getContentUri(defineVolume(ctx,externalVolumeName)) + else -> MediaStore.Downloads.getContentUri(defineVolume(ctx,externalVolumeName)) + } + + } + private fun saveId3(file: String, id3v2Tags: Map?) { + + if (id3v2Tags != null) { + try { + val mp3File = Mp3File(file) + + val id3v24Tag = ID3v24Tag() + id3v24Tag.title = id3v2Tags["title"] + id3v24Tag.comment = id3v2Tags["comment"] + id3v24Tag.album = id3v2Tags["album"] + id3v24Tag.artist = id3v2Tags["artist"] + id3v24Tag.url = java.lang.String.format( + "https://www.suamusica.com.br/perfil/%s?playlistId=%s&albumId=%s&musicId=%s", + id3v2Tags["artistId"], + id3v2Tags["playlistId"], + id3v2Tags["albumId"], + id3v2Tags["musicId"] + ) + mp3File.id3v2Tag = id3v24Tag + val newFilename = "$file.tmp" + mp3File.save(newFilename) + + val from = File(newFilename) + from.renameTo(File(file)) + + Log.i(TAG, "Successfully set ID3v2 tags") + } catch (e: Exception) { + Log.e(TAG, "Failed to set ID3v2 tags", e) + } + } + } +} diff --git a/android/src/main/kotlin/com/snnafi/media_store_plus/MediaStorePlusPlugin.kt b/android/src/main/kotlin/com/snnafi/media_store_plus/MediaStorePlusPlugin.kt index a1ea540..dda1444 100644 --- a/android/src/main/kotlin/com/snnafi/media_store_plus/MediaStorePlusPlugin.kt +++ b/android/src/main/kotlin/com/snnafi/media_store_plus/MediaStorePlusPlugin.kt @@ -16,8 +16,11 @@ import android.util.Log import androidx.annotation.NonNull import androidx.annotation.RequiresApi import androidx.documentfile.provider.DocumentFile -import com.mpatric.mp3agic.ID3v24Tag -import com.mpatric.mp3agic.Mp3File +import androidx.work.Configuration +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.workDataOf +import com.google.gson.Gson import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware @@ -43,23 +46,24 @@ fun String.capitalized(): String { /** MediaStorePlusPlugin */ class MediaStorePlusPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener { - private var activity: Activity? = null - private lateinit var channel: MethodChannel - private lateinit var result: Result - private lateinit var uriString: String - private lateinit var fileName: String - private lateinit var tempFilePath: String - private var dirType: Int = 0 - private lateinit var dirName: String - private lateinit var appFolder: String - private var externalVolumeName: String? = null - private var id3v2Tags: Map? = null - private val TAG = "MediaStorage" + private var activity: Activity? = null + private lateinit var channel: MethodChannel + private lateinit var result: Result + private lateinit var uriString: String + private lateinit var fileName: String + private lateinit var tempFilePath: String + private var dirType: Int = 0 + private lateinit var dirName: String + private lateinit var appFolder: String + private var externalVolumeName: String? = null + private var id3v2Tags: Map? = null + private val TAG = "MediaStorage" override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "media_store_plus") channel.setMethodCallHandler(this) + } override fun onMethodCall(call: MethodCall, result: Result) { @@ -67,6 +71,12 @@ class MediaStorePlusPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, Log.d(TAG, "call.method: ${call.method}") if (call.method == "getPlatformSDKInt") { result.success(Build.VERSION.SDK_INT) + val myConfig = Configuration.Builder() + .setMinimumLoggingLevel(Log.INFO) + .build() + + WorkManager.initialize(this.activity!!.applicationContext, myConfig) + } else if (call.method == "saveFile") { saveFile( Uri.parse(call.argument("tempFilePath")!!).path!!, @@ -77,7 +87,7 @@ class MediaStorePlusPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, call.argument("externalVolumeName"), call.argument("id3v2Tags"), ) - } else if (call.method == "deleteFile") { + } else if (call.method == "deleteFile") { deleteFile( call.argument("fileName")!!, call.argument("appFolder")!!, @@ -198,7 +208,7 @@ class MediaStorePlusPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, externalVolumeName, id3v2Tags ) - File(path).delete() +// File(path).delete() result.success(true) } catch (e: Exception) { @@ -265,38 +275,7 @@ class MediaStorePlusPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, } } - @RequiresApi(Build.VERSION_CODES.O) - private fun saveId3(file: String, id3v2Tags: Map?) { - - if (id3v2Tags != null) { - try { - val mp3File = Mp3File(file) - - val id3v24Tag = ID3v24Tag() - id3v24Tag.title = id3v2Tags["title"] - id3v24Tag.comment = id3v2Tags["comment"] - id3v24Tag.album = id3v2Tags["album"] - id3v24Tag.artist = id3v2Tags["artist"] - id3v24Tag.url = java.lang.String.format( - "https://www.suamusica.com.br/perfil/%s?playlistId=%s&albumId=%s&musicId=%s", - id3v2Tags["artistId"], - id3v2Tags["playlistId"], - id3v2Tags["albumId"], - id3v2Tags["musicId"] - ) - mp3File.id3v2Tag = id3v24Tag - val newFilename = "$file.tmp" - mp3File.save(newFilename) - val from = File(newFilename) - from.renameTo(File(file)) - - Log.i(TAG, "Successfully set ID3v2 tags") - } catch (e: Exception) { - Log.e(TAG, "Failed to set ID3v2 tags", e) - } - } - } @RequiresApi(Build.VERSION_CODES.Q) @@ -310,6 +289,7 @@ class MediaStorePlusPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, } + @RequiresApi(Build.VERSION_CODES.O) private fun createOrUpdateFile( path: String, name: String, @@ -319,39 +299,27 @@ class MediaStorePlusPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, externalVolumeName: String?, id3v2Tags: Map? ) { - saveId3(path, id3v2Tags) - // { photo, music, video, download } - Log.d(TAG, "DirName $dirName") - - val relativePath: String = if (appFolder.trim().isEmpty()) { - dirName - } else { - dirName + File.separator + appFolder - } - - deleteFileUsingDisplayName(name, appFolder, dirType, dirName, externalVolumeName) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val values = ContentValues().apply { - put(MediaStore.Audio.Media.DISPLAY_NAME, name) - put(MediaStore.Audio.Media.RELATIVE_PATH, relativePath) - put(MediaStore.Audio.Media.IS_PENDING, 1) - } + val gson = Gson() + val id3v2TagsJson = gson.toJson(id3v2Tags) + + val workData = workDataOf( + "path" to path, + "name" to name, + "appFolder" to appFolder, + "dirType" to dirType, + "dirName" to dirName, + "externalVolumeName" to externalVolumeName, + "id3v2Tags" to id3v2TagsJson + ) + val fileOperationRequest = OneTimeWorkRequestBuilder() + .setInputData(workData) + .build() - val resolver = activity!!.applicationContext.contentResolver - val uri = resolver.insert(getUriFromDirType(dirType, externalVolumeName), values)!! - resolver.openOutputStream(uri).use { os -> - File(path).inputStream().use { it.copyTo(os!!) } - } - values.clear() - values.put(MediaStore.Audio.Media.IS_PENDING, 0) - resolver.update(uri, values, null, null) + WorkManager.getInstance(activity!!.applicationContext).enqueue(fileOperationRequest) - Log.d(TAG, "saveFile $name") - - } }