From 3da65295b5faaced463494d4fa6e4ac735e57824 Mon Sep 17 00:00:00 2001 From: Dev Femi Badmus Date: Sat, 24 Aug 2024 22:31:37 +0100 Subject: [PATCH] :package: use lcalhost :tada: working fine :construction_worker: --- .../com/example/mediasaver/MainActivity.kt | 1347 ++++++++++------- 1 file changed, 829 insertions(+), 518 deletions(-) diff --git a/android/app/src/main/kotlin/com/example/mediasaver/MainActivity.kt b/android/app/src/main/kotlin/com/example/mediasaver/MainActivity.kt index eca621a..c24bbe1 100644 --- a/android/app/src/main/kotlin/com/example/mediasaver/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/mediasaver/MainActivity.kt @@ -53,109 +53,435 @@ import android.content.ClipboardManager import kotlinx.coroutines.CoroutineScope import java.util.concurrent.TimeUnit -import kotlinx.coroutines.launch import okhttp3.OkHttpClient import okhttp3.Request + + +import android.content.ContentUris +import android.content.ContentValues +import android.content.res.Resources +import android.provider.MediaStore + import java.io.InputStream +import java.io.OutputStream +import java.net.URL + +import okhttp3.Response +import android.content.ContentResolver + +import android.content.SharedPreferences +import android.widget.Toast + +import fi.iki.elonen.NanoHTTPD +import java.net.BindException +import java.util.concurrent.Executors + +import android.provider.DocumentsContract +import android.app.Activity + +import android.database.Cursor + +import androidx.documentfile.provider.DocumentFile + +import java.io.ByteArrayInputStream + +import android.media.ThumbnailUtils + + + object Common { - // Environment.DIRECTORY_PICTURES was introduced in API level 19 (Android 4.4), so it should work for Android versions 4.4 and higher. However, if it's not working for versions below Android 10 on some device, we can use a fallback approach to handle this situation. - val SAVEDSTATUSES: File = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - File(Environment.getExternalStorageDirectory().toString(), "Media Saver") - // File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Media Saver") - } else { - File(Environment.getExternalStorageDirectory().toString(), "Media Saver") - } + private const val PREFS_NAME = "ServerPrefs" + private const val KEY_SERVER_PORT = "server_port" + private const val KEY_PERMISSION = "folder_access" + private const val KEY_SERVER_RUNNING = "server_running" + + const val REQUEST_CODE_WHATSAPP_FOLDER = 1001 - val WHATSAPP: File = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - File(Environment.getExternalStorageDirectory().toString() + "/Android/media/com.whatsapp/WhatsApp/Media/.Statuses") - } else { - File(Environment.getExternalStorageDirectory().toString() + "/WhatsApp/Media/.Statuses") + private const val PORT = 8080 + private var server: NanoHTTPD? = null + + // Function to get the SharedPreferences instance + private fun getSharedPreferences(context: Context): SharedPreferences { + return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + } + + // Permission border + fun updatePermission(isPermit: Boolean, context: Context) { + val prefs = getSharedPreferences(context) + prefs.edit().putBoolean(KEY_PERMISSION, isPermit).apply() + } + fun hasPermission(context: Context): Boolean { + val prefs = getSharedPreferences(context) + return prefs.getBoolean(KEY_PERMISSION, false) + } + + // Function to save Server State + fun saveServerState(isRunning: Boolean, context: Context) { + val prefs = getSharedPreferences(context) + prefs.edit().putBoolean(KEY_SERVER_RUNNING, isRunning).apply() + } + // Function to load Server State + fun loadServerState(context: Context): Boolean { + val prefs = getSharedPreferences(context) + return prefs.getBoolean(KEY_SERVER_RUNNING, false) + } + + // Function to start the server + fun startServer(context: Context): String { + return try { + val server = object : NanoHTTPD(PORT) { + override fun serve(session: IHTTPSession?): Response { + val uriString = session?.uri?.removePrefix("/files/") + + return try { + if (uriString != null) { + if (uriString.contains("Android/media/")) { + val mainUrl = uriString.split("/getThumbnail").first() + val pathSegments = mainUrl.split("Android/media/").last().split("/") + + val getThumbnail = uriString.contains("/getThumbnail") + + // Root folder where permission is granted + val treeUri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fmedia") + val documentFile = DocumentFile.fromTreeUri(context, treeUri) + + // Navigate through subdirectories + var currentDir: DocumentFile? = documentFile + for (segment in pathSegments.dropLast(1)) { + currentDir = currentDir?.findFile(segment) + if (currentDir == null || !currentDir.isDirectory) { + return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Directory not found") + } + } + + // Find the target file + val targetFile = currentDir?.findFile(pathSegments.last()) + if (targetFile != null && targetFile.isFile) { + // Open the input stream and return the file content + val inputStream = context.contentResolver.openInputStream(targetFile.uri) + val mimeType = context.contentResolver.getType(targetFile.uri) ?: "application/octet-stream" + + if (getThumbnail) { + Log.d("targetFile.uri.toString()", targetFile.uri.toString()) + val retriever = MediaMetadataRetriever() + try { + retriever.setDataSource(context, targetFile.uri) + val bitmap = retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) + if (bitmap != null) { + val outputStream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream) + val imageData = outputStream.toByteArray() + return newFixedLengthResponse(Response.Status.OK, "image/jpeg", ByteArrayInputStream(imageData), imageData.size.toLong()) + } else { + return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Thumbnail not available") + } + } catch (e: Exception) { + return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Error retrieving thumbnail: ${e.message}") + } finally { + retriever.release() + } + } + + if (inputStream != null) { + val length = inputStream.available().toLong() + return newFixedLengthResponse(Response.Status.OK, mimeType, inputStream, length) + } else { + return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "File not found") + } + } else { + return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "File not found") + } + + } else { + // This is a MediaStore ID (SAVED case) + val fileId = uriString.toLongOrNull() + + if (fileId != null) { + // Query the file using the ID + val contentResolver = context.contentResolver + val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, fileId) + val mimeType = contentResolver.getType(uri) ?: "application/octet-stream" + val inputStream = contentResolver.openInputStream(uri) + + if (inputStream != null) { + val length = inputStream.available().toLong() + return newFixedLengthResponse(Response.Status.OK, mimeType, inputStream, length) + } else { + return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "File not found") + } + } else { + return newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/plain", "Invalid file ID") + } + } + } else { + return newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/plain", "Invalid request") + } + } catch (e: IOException) { + return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "I/O error: ${e.message}") + } + } + } + + // Start the server + server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false) + // Save server state + saveServerState(true, context) + + "Server started successfully on port $PORT" + } catch (e: BindException) { + // Handle the case where the port is already in use + "Error: Port $PORT is already in use. Please try another port." + } catch (e: SecurityException) { + // Handle the case where the server doesn't have permission to bind to the port + "Error: Insufficient permissions to start the server on port $PORT." + } catch (e: IOException) { + // Handle general I/O errors during server startup + "Error: Failed to start the server due to an I/O error: ${e.message}" + } catch (e: Exception) { + // Catch any unexpected errors + "Error: Unexpected error while starting the server: ${e.message}" + } + } + + // Function to stop Server State + fun stopServer(context: Context): String { + return try { + // Check if the server is running before stopping it + if (loadServerState(context)) { + server?.stop() + server = null + + // Save server state + saveServerState(false, context) + + "Server stopped successfully." + } else { + "Server is not running." + } + } catch (e: Exception) { + "Error: Failed to stop the server: ${e.message}" } + } - val WHATSAPP4B: File = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - File(Environment.getExternalStorageDirectory().toString() + "/Android/media/com.whatsapp.w4b/WhatsApp Business/Media/.Statuses") - } else { - File(Environment.getExternalStorageDirectory().toString() + "/WhatsApp Business/Media/.Statuses") + // cursor Query for media folder + fun savedMediasQuery(contentResolver: ContentResolver): Cursor? { + val folderPath = "Pictures/Media Saver" + val mediaStoreUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + + // Check if the folder exists + val projection = arrayOf(MediaStore.Images.Media._ID) + val selection = "${MediaStore.Images.Media.RELATIVE_PATH} LIKE ?" + val selectionArgs = arrayOf("$folderPath%") + + var folderExists = false + contentResolver.query(mediaStoreUri, projection, selection, selectionArgs, null)?.use { cursor -> + if (cursor.moveToFirst()) { + folderExists = true + } } -} + // Query for files in the folder + return contentResolver.query( + mediaStoreUri, + arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME), + selection, + selectionArgs, + null + ) + } + + fun whatsappMediaQuery(contentResolver: ContentResolver): Uri? { + val treeUri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fmedia") + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)) + + // Fetch child directories under "media" + val projection = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME) + val cursor = contentResolver.query(childrenUri, projection, null, null, null) + + cursor?.use { + while (cursor.moveToNext()) { + val displayName = cursor.getString(cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)) + if (displayName == "com.whatsapp") { + // Once we find "com.whatsapp.w4b", build the Uri for its children + val documentId = cursor.getString(cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) + val whatsappChildrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId) + + // Now go deeper to find "WhatsApp Business" -> "Media" -> ".Statuses" + val innerCursor = contentResolver.query(whatsappChildrenUri, projection, null, null, null) + innerCursor?.use { + while (innerCursor.moveToNext()) { + val innerDisplayName = innerCursor.getString(innerCursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)) + if (innerDisplayName == "WhatsApp") { + val innerDocumentId = innerCursor.getString(innerCursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) + val whatsappBusinessChildrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, innerDocumentId) + + // Enter the "Media" folder + val mediaCursor = contentResolver.query(whatsappBusinessChildrenUri, projection, null, null, null) + mediaCursor?.use { + while (mediaCursor.moveToNext()) { + val mediaDisplayName = mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)) + if (mediaDisplayName == "Media") { + val mediaDocumentId = mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) + val mediaChildrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, mediaDocumentId) + + // Finally, go into the ".Statuses" folder + val statusesCursor = contentResolver.query(mediaChildrenUri, projection, null, null, null) + statusesCursor?.use { + while (statusesCursor.moveToNext()) { + val statusesDisplayName = statusesCursor.getString(statusesCursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)) + if (statusesDisplayName == ".Statuses") { + // Return the Uri for the .Statuses directory + val statusesDocumentId = statusesCursor.getString(statusesCursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) + return DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, statusesDocumentId) + } + } + } + } + } + } + } + } + } + } + } + } + return null + } + + fun whatsappBusinessMediaQuery(contentResolver: ContentResolver): Uri? { + val treeUri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fmedia") + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)) + + // Fetch child directories under "media" + val projection = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME) + val cursor = contentResolver.query(childrenUri, projection, null, null, null) + + cursor?.use { + while (cursor.moveToNext()) { + val displayName = cursor.getString(cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)) + if (displayName == "com.whatsapp.w4b") { + // Once we find "com.whatsapp.w4b", build the Uri for its children + val documentId = cursor.getString(cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) + val whatsappChildrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId) + + // Now go deeper to find "WhatsApp Business" -> "Media" -> ".Statuses" + val innerCursor = contentResolver.query(whatsappChildrenUri, projection, null, null, null) + innerCursor?.use { + while (innerCursor.moveToNext()) { + val innerDisplayName = innerCursor.getString(innerCursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)) + if (innerDisplayName == "WhatsApp Business") { + val innerDocumentId = innerCursor.getString(innerCursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) + val whatsappBusinessChildrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, innerDocumentId) + + // Enter the "Media" folder + val mediaCursor = contentResolver.query(whatsappBusinessChildrenUri, projection, null, null, null) + mediaCursor?.use { + while (mediaCursor.moveToNext()) { + val mediaDisplayName = mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)) + if (mediaDisplayName == "Media") { + val mediaDocumentId = mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) + val mediaChildrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, mediaDocumentId) + + // Finally, go into the ".Statuses" folder + val statusesCursor = contentResolver.query(mediaChildrenUri, projection, null, null, null) + statusesCursor?.use { + while (statusesCursor.moveToNext()) { + val statusesDisplayName = statusesCursor.getString(statusesCursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)) + if (statusesDisplayName == ".Statuses") { + // Return the Uri for the .Statuses directory + val statusesDocumentId = statusesCursor.getString(statusesCursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) + return DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, statusesDocumentId) + } + } + } + } + } + } + } + } + } + } + } + } + return null + } + +} class MainActivity : FlutterActivity() { - private val FILE_PROVIDER_AUTHORITY = "com.blackstackhub.mediasaver.fileprovider" private val TAG = "MainActivity" - private val APP_STORAGE_ACCESS_REQUEST_CODE = 501 private val CHANNEL = "com.blackstackhub.mediasaver" override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> when (call.method) { - "shareApp" -> result.success(shareApp()) - "sendEmail" -> result.success(sendEmail()) - "launchDemo" -> result.success(launchDemo()) - "launchUpdate" -> result.success(launchUpdate()) - "launchPrivacyPolicy" -> result.success(launchPrivacyPolicy()) - "getClipboardContent" -> result.success(getClipboardContent()) - "checkStoragePermission" -> result.success(checkStoragePermission()) - "requestStoragePermission" -> result.success(requestStoragePermission()) - "downloadFile" -> { - val fileUrl = call.argument("fileUrl") - val fileId = call.argument("fileId") - if (fileUrl != null && fileId !=null) { + "saveMedia" -> { + val url = call.argument("url") + if (url != null) { CoroutineScope(Dispatchers.Main).launch { - val filePath = downloadAndSaveFile(fileUrl, fileId) - result.success(filePath) + result.success(saveMedia(url)) } } else { result.error("INVALID_PARAMETERS", "Invalid parameters", null) } } - "getStatusFilesInfo" -> { - val appType = call.argument("appType") - if (appType != null) { - result.success(getStatusFilesInfo(appType)) + "shareMedia" -> { + val url = call.argument("url") + if (url != null) { + result.success(shareMedia(url)) } else { result.error("INVALID_PARAMETERS", "Invalid parameters", null) } } - "getVideoThumbnailAsync" -> { - val absolutePath = call.argument("absolutePath") - if (absolutePath != null) { - lifecycleScope.launch { - try { - val thumbnail = getVideoThumbnailAsync(absolutePath) - result.success(thumbnail) - } catch (e: Exception) { - result.error("EXCEPTION", "Error during thumbnail retrieval", null) - } - } + "deleteMedia" -> { + val url = call.argument("url") + if (url != null) { + result.success(deleteMedia(url)) } else { result.error("INVALID_PARAMETERS", "Invalid parameters", null) } } - "saveStatus" -> { - val filePath = call.argument("filePath") - // val folder = call.argument("folder") - if (filePath != null) { - result.success(saveStatus(filePath)) + "getMedias" -> { + val appType = call.argument("appType") + if (appType != null) { + CoroutineScope(Dispatchers.Main).launch { + result.success(getMedias(appType)) + } } else { result.error("INVALID_PARAMETERS", "Invalid parameters", null) } } - "deleteStatus" -> { - val filePath = call.argument("filePath") - if (filePath != null) { - result.success(deleteStatus(filePath)) + "downloadAndSaveFile" -> { + val fileUrl = call.argument("fileUrl") + if (fileUrl != null) { + CoroutineScope(Dispatchers.Main).launch { + result.success(downloadAndSaveFile(fileUrl)) + } } else { result.error("INVALID_PARAMETERS", "Invalid parameters", null) } } - "shareMedia" -> { - val filePath = call.argument("filePath") - if (filePath != null) { - result.success(shareMedia(filePath)) + + "hasFolderAccess" -> result.success(hasFolderAccess()) + "requestAccessToFolder" -> { + CoroutineScope(Dispatchers.Main).launch { + requestAccessToFolder() + result.success(null) + } + } + + "copyText" -> result.success(copyText()) + "shareApp" -> result.success(shareApp()) + "sendEmail" -> result.success(sendEmail()) + "launchurl" -> { + val url = call.argument("url") + if (url != null) { + CoroutineScope(Dispatchers.Main).launch { + result.success(launchurl(url)) + } } else { result.error("INVALID_PARAMETERS", "Invalid parameters", null) } @@ -165,544 +491,529 @@ class MainActivity : FlutterActivity() { } } - fun getAppVersion(context: Context): String { - try { - val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) - return packageInfo.versionName - } catch (e: PackageManager.NameNotFoundException) { - // e.printStackTrace() - } - return "Unknown" - } + // SAVE, GET, SHARE, DELETE, DOWNLOAD MEDIA +fun saveMedia(url: String): Boolean { + val theFileName = url.substringAfterLast('/') + Log.d("MediaSaver", "theFileName: $theFileName") + val contentResolver = context.contentResolver - private fun sendEmail(): Boolean{ - val emailIntent = Intent(Intent.ACTION_SENDTO) + // Base URI for the external storage + val treeUri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fmedia") + val documentId = DocumentsContract.getTreeDocumentId(treeUri) + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId) - val locale = Locale.getDefault() - val country = locale.displayCountry + // Projection to retrieve document IDs and display names + val projection = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME) - val email = "devfemibadmus@gmail.com" - val subject = "Request a new feature for Media-Saver" - - val preBody = "Version: ${getAppVersion(applicationContext)}\nCountry: $country\n\nI want to request feature for..." - val mailtoLink = "mailto:$email?subject=$subject&body=$preBody" + // Find the ".Statuses" directory + val sourceUri: Uri? - // Set the email address - emailIntent.data = Uri.parse(mailtoLink) - - // Check if there is a package that can handle the intent - try { - startActivity(emailIntent) - } catch (e: Exception) { - // If no email app is available, open a web browser - val webIntent = Intent(Intent.ACTION_VIEW) - webIntent.data = Uri.parse("https://github.com/devfemibadmus/Media-Saver") - startActivity(webIntent) +// Determine the value of the variable based on the condition +sourceUri = if (url.contains("com.whatsapp.w4b")) { + Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fmedia/document/primary%3AAndroid%2Fmedia%2Fcom.whatsapp.w4b%2FWhatsApp%20Business%2FMedia%2F.Statuses/children") +} else { + Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fmedia/document/primary%3AAndroid%2Fmedia%2Fcom.whatsapp%2FWhatsApp%2FMedia%2F.Statuses/children") +} +/* + val cursor = contentResolver.query(childrenUri, projection, null, null, null) + cursor?.use { c -> + while (c.moveToNext()) { + val displayName = c.getString(c.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)) + if (displayName == "com.whatsapp") { + val documentId = c.getString(c.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) + val whatsappChildrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId) + + val innerCursor = contentResolver.query(whatsappChildrenUri, projection, null, null, null) + innerCursor?.use { ic -> + while (ic.moveToNext()) { + val innerDisplayName = ic.getString(ic.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)) + if (innerDisplayName == "WhatsApp") { + val innerDocumentId = ic.getString(ic.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) + val whatsappBusinessChildrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, innerDocumentId) + + val mediaCursor = contentResolver.query(whatsappBusinessChildrenUri, projection, null, null, null) + mediaCursor?.use { mc -> + while (mc.moveToNext()) { + val mediaDisplayName = mc.getString(mc.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)) + if (mediaDisplayName == "Media") { + val mediaDocumentId = mc.getString(mc.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) + val mediaChildrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, mediaDocumentId) + + val statusesCursor = contentResolver.query(mediaChildrenUri, projection, null, null, null) + statusesCursor?.use { sc -> + while (sc.moveToNext()) { + val statusesDisplayName = sc.getString(sc.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)) + if (statusesDisplayName == ".Statuses") { + val statusesDocumentId = sc.getString(sc.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) + sourceUri = DocumentsContract.buildChildDocumentsUriUsingTree(mediaChildrenUri, statusesDocumentId) + return@use + } + } + } + } + } + } + } + } + } + } } - return true - } - - private fun launchDemo(): Boolean{ - val webIntent = Intent(Intent.ACTION_VIEW) - webIntent.data = Uri.parse("https://github.com/devfemibadmus/mediasaver") - startActivity(webIntent) - return true } + */ + val mediaUri: Uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + val projectionName = arrayOf(MediaStore.Images.Media.DISPLAY_NAME) + val selection = "${MediaStore.Images.Media.DISPLAY_NAME} = ?" + val selectionArgs = arrayOf("MediaSaver_$theFileName") + + val cursor = contentResolver.query(mediaUri, projectionName, selection, selectionArgs, null) + val fileExists = cursor?.use { it.count > 0 } + if (fileExists == true) { + Log.d("Mediasaver", "File with the name 'MediaSaver_$theFileName' already exists.") + return false + } + + Log.d("MdiaSaver", "sourceUri: $sourceUri") + + + + // Get the first file in the .Statuses directory + val fileProjection = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME) + val fileCursor = contentResolver.query(sourceUri!!, fileProjection, null, null, null) + fileCursor?.use { fc -> + while (fc.moveToNext()) { + val fileName = fc.getString(fc.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)) + + // Check if the file name matches the desired name + if (fileName == theFileName) { + val fileDocumentId = fc.getString(fc.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) + val fileUri = DocumentsContract.buildDocumentUriUsingTree(sourceUri, fileDocumentId) + Log.d("Mediasaver", "$fileName") + Log.d("Mediasaver", "$fileUri") + + // Target directory and MIME type + val mimeType = when { + fileName.endsWith(".mp4", ignoreCase = true) -> "video/mp4" + fileName.endsWith(".jpg", ignoreCase = true) || fileName.endsWith(".jpeg", ignoreCase = true) -> "image/jpeg" + fileName.endsWith(".png", ignoreCase = true) -> "image/png" + else -> throw IllegalArgumentException("Unsupported file type") + } + Log.d("Mediasaver", "$mimeType") - private fun launchPrivacyPolicy(): Boolean{ - val webIntent = Intent(Intent.ACTION_VIEW) - webIntent.data = Uri.parse("https://devfemibadmus.blackstackhub.com/webmedia#privacy") - startActivity(webIntent) - return true - } + val mediaCollectionUri = when { + mimeType.startsWith("image/") -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + mimeType.startsWith("video/") -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + else -> throw IllegalArgumentException("Unsupported MIME type") + } + Log.d("Mediasaver", "$mediaCollectionUri") - private fun getClipboardContent(): String{ - val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - if (clipboardManager.hasPrimaryClip()) { - val clipData: ClipData? = clipboardManager.primaryClip - if (clipData != null && clipData.itemCount > 0) { - val item = clipData.getItemAt(0) - return item.text.toString() + // Create the target file in the MediaStore + val targetFileValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, "MediaSaver_$fileName") + put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/Media Saver/") + put(MediaStore.MediaColumns.MIME_TYPE, mimeType) } - } - return "" - } - - private fun launchUpdate(): Boolean{ - val webIntent = Intent(Intent.ACTION_VIEW) - webIntent.data = Uri.parse("https://play.google.com/store/apps/details?id=com.blackstackhub.mediasaver") - startActivity(webIntent) - return true - } + Log.d("Mediasaver", "$targetFileValues") - private fun shareApp(): Boolean{ - // Share intent - val shareIntent = Intent(Intent.ACTION_SEND) - shareIntent.type = "text/plain" - - // Set the subject and message - val shareSubject = "Check out this free Media Saver" - val shareMessage = "$shareSubject\n\nNo Ads, No Cost—Download Videos and Photos From Instagram, Facebook, and TikTok for Free!\n\nGet the app: https://play.google.com/store/apps/details?id=com.blackstackhub.mediasaver" - - // shareIntent.putExtra(Intent.EXTRA_SUBJECT, shareSubject) - shareIntent.putExtra(Intent.EXTRA_TEXT, shareMessage) + val targetUri = contentResolver.insert(mediaCollectionUri, targetFileValues) + ?: throw IOException("Failed to create target file") - try { - startActivity(Intent.createChooser(shareIntent, "Share via")) - } catch (e: Exception) { - // Handle exceptions - // e.printStackTrace() - // You might want to return an error message or handle it differently + Log.d("Mediasaver", "$targetUri") + // Copy file content from source to target + Log.d("Mediasaver", "InputStream wants to start") + contentResolver.openInputStream(fileUri)?.use { inputStream -> + Log.d("Mediasaver", "InputStream opened successfully: $inputStream") + contentResolver.openOutputStream(targetUri)?.use { outputStream -> + Log.d("Mediasaver", "OutputStream opened successfully: $outputStream") + inputStream.copyTo(outputStream) + Log.d("Mediasaver", "File copied successfully") + return true + } ?: throw IOException("Failed to open output stream for target file") + } ?: throw IOException("Failed to open input stream for source file") + } } - return true } + return false +} - private fun shareMedia(filePath: String): String { - val file = File(filePath) - - return if (file.exists()) { - val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( - MimeTypeMap.getFileExtensionFromUrl(file.absolutePath) - ) - val uri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - // Use FileProvider on Android N and above - val fileUri = FileProvider.getUriForFile( - applicationContext, - FILE_PROVIDER_AUTHORITY, - file - ) - fileUri - } else { - Uri.fromFile(file) - } - // Share intent - val shareIntent = Intent(Intent.ACTION_SEND) - val shareSubject = "Saved with Media Saver—no cost, no ads!" - val shareMessage = "$shareSubject\n\nDownload it: https://play.google.com/store/apps/details?id=com.blackstackhub.mediasaver" - //shareIntent.type = mimeType - shareIntent.type = mimeType - // shareIntent.putExtra(Intent.EXTRA_SUBJECT, shareSubject) - shareIntent.putExtra(Intent.EXTRA_TEXT, shareMessage) - shareIntent.putExtra(Intent.EXTRA_STREAM, uri) - // Grant temporary read permission to the content URI - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - try { - startActivity(Intent.createChooser(shareIntent, "Share Media")) - "sharing..." - } catch (e: Exception) { - // Handle exceptions - // e.printStackTrace() - "can't share" + +private fun shareMedia(url: String): Boolean { + val contentResolver = applicationContext.contentResolver + + // Check if the URL contains "Android/media" indicating it's not an ID + return if (url.contains("Android/media")) { + // Handle URL with DocumentFile traversal + val uri = Uri.parse(url) + val pathSegments = uri.pathSegments + val lastSegment = pathSegments.last() + + // Handle specific file retrieval with DocumentFile + val treeUri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fmedia") + val documentFile = DocumentFile.fromTreeUri(applicationContext, treeUri) + + // Navigate through subdirectories + var currentDir: DocumentFile? = documentFile + for (segment in pathSegments.dropLast(1)) { + currentDir = currentDir?.findFile(segment) + if (currentDir == null || !currentDir.isDirectory) { + return false } - } else { - "can't share" } - } - private fun getFileFormat(fileName: String): String { - val lastDotIndex = fileName.lastIndexOf('.') - return if (lastDotIndex != -1 && lastDotIndex < fileName.length - 1) { - fileName.substring(lastDotIndex + 1).toLowerCase() - } else { - "unknown" + // Find the target file + val targetFile = currentDir?.findFile(lastSegment) + if (targetFile != null && targetFile.isFile) { + val mimeType = contentResolver.getType(targetFile.uri) ?: "application/octet-stream" + val subject = "Saved with Media Saver—no cost, no ads!" + val body = "$subject\n\nDownload it: https://play.google.com/store/apps/details?id=com.blackstackhub.mediasaver" + val intent = Intent(Intent.ACTION_SEND).apply { + type = mimeType + putExtra(Intent.EXTRA_STREAM, targetFile.uri) + putExtra(Intent.EXTRA_SUBJECT, subject) + putExtra(Intent.EXTRA_TEXT, body) + } + val shareIntent = Intent.createChooser(intent, null) + startActivity(shareIntent) + return true } - } - - private suspend fun createVideoThumbnailAsync(videoPath: String): Bitmap? = withContext(Dispatchers.IO) { - val retriever = MediaMetadataRetriever() - return@withContext try { - retriever.setDataSource(videoPath) - retriever.getFrameAtTime() + + false + } else { + // Handle URL with MediaStore ID + val fileId = try { + val pathSegments = Uri.parse(url).pathSegments + if (pathSegments.isNotEmpty()) { + pathSegments.last().toLongOrNull() + } else { + null + } } catch (e: Exception) { - // e.printStackTrace() null - } finally { - retriever.release() - } - } - - private suspend fun getVideoThumbnailAsync(absolutePath: String): ByteArray { - return withContext(Dispatchers.Default) { - try { - val bitmap = createVideoThumbnailAsync(absolutePath) - if (bitmap != null) { - val stream = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) - bitmap.recycle() - return@withContext stream.toByteArray() - } - } catch (e: Exception) { - // e.printStackTrace() + } ?: return false + + val fileUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, fileId) + + return try { + val mimeType = contentResolver.getType(fileUri) ?: "application/octet-stream" + val subject = "Saved with Media Saver—no cost, no ads!" + val body = "$subject\n\nDownload it: https://play.google.com/store/apps/details?id=com.blackstackhub.mediasaver" + val intent = Intent(Intent.ACTION_SEND).apply { + type = mimeType + putExtra(Intent.EXTRA_STREAM, fileUri) + putExtra(Intent.EXTRA_SUBJECT, subject) + putExtra(Intent.EXTRA_TEXT, body) } - return@withContext ByteArray(0) + val shareIntent = Intent.createChooser(intent, null) + startActivity(shareIntent) + true + } catch (e: Exception) { + false } } +} - // the main issue that could lead to skipped frames and performance problems is the synchronous execution of createVideoThumbnail(videoPath) function on the main thread. The MediaMetadataRetriever and getFrameAtTime() methods perform I/O operations and may involve heavy computations, especially for large videos. - /* - private fun getVideoThumbnail(absolutePath: String): ByteArray { - - fun createVideoThumbnail(videoPath: String): Bitmap? { - val retriever = MediaMetadataRetriever() - - try { - retriever.setDataSource(videoPath) - return retriever.getFrameAtTime() - } catch (e: Exception) { - // e.printStackTrace() - } finally { - retriever.release() +private fun deleteMedia(url: String): Boolean { + val contentResolver = applicationContext.contentResolver + + // Check if the URL contains "Android/media" indicating it's not an ID + return if (url.contains("Android/media")) { + // Handle URL with DocumentFile traversal + val uri = Uri.parse(url) + val pathSegments = uri.pathSegments + val lastSegment = pathSegments.last() + + // Handle specific file deletion with DocumentFile + val treeUri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fmedia") + val documentFile = DocumentFile.fromTreeUri(applicationContext, treeUri) + + // Navigate through subdirectories + var currentDir: DocumentFile? = documentFile + for (segment in pathSegments.dropLast(1)) { + currentDir = currentDir?.findFile(segment) + if (currentDir == null || !currentDir.isDirectory) { + return false } + } - return null + // Find the target file + val targetFile = currentDir?.findFile(lastSegment) + if (targetFile != null && targetFile.isFile) { + return targetFile.delete() } - try { - val bitmap = createVideoThumbnail(absolutePath); - if (bitmap != null){ - val stream = ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); - bitmap.recycle(); - return stream.toByteArray(); + + false + } else { + // Handle URL with MediaStore ID + val fileId = try { + val pathSegments = Uri.parse(url).pathSegments + if (pathSegments.isNotEmpty()) { + pathSegments.last().toLongOrNull() + } else { + null } } catch (e: Exception) { - // e.printStackTrace() + null + } ?: return false + + val fileUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, fileId) + + return try { + val rowsDeleted = contentResolver.delete(fileUri, null, null) + rowsDeleted > 0 + } catch (e: Exception) { + false } - return ByteArray(0) } - */ - - private fun getStatusFilesInfo(appType: String): List> { - val statusFilesInfo = mutableListOf>() +} - fun createVideoThumbnail(videoPath: String): Bitmap? { - val retriever = MediaMetadataRetriever() - try { - retriever.setDataSource(videoPath) - return retriever.getFrameAtTime() - } catch (e: Exception) { - // e.printStackTrace() - } finally { - retriever.release() - } + private fun getMedias(appType: String): List> { + Common.startServer(context) + Log.d("appType", appType) - return null - } + val fileInfoList = mutableListOf>() + when (appType) { + "SAVED" -> { + val cursor = Common.savedMediasQuery(contentResolver) + cursor?.use { cursorInstance -> + while (cursorInstance.moveToNext()) { + val id = cursorInstance.getLong(cursorInstance.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) + val uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id.toString()) + val mimeType = contentResolver.getType(uri) ?: "application/octet-stream" + val url = "http://localhost:8080/files/$id" - fun getMediaByte(file: File, format: String): ByteArray { - try { - // Check if the file is an mp4 video - // val format = getFileFormat(file.name) - if (format == "mp4") { - // I/Choreographer(25094): Skipped 717 frames! The application may be doing too much work on its main thread. - /* - val retriever = MediaMetadataRetriever() - retriever.setDataSource(file.absolutePath) - val thumbnailBitmap = retriever.getFrameAtTime() - - thumbnailBitmap?.let { - val stream = ByteArrayOutputStream() - it.compress(Bitmap.CompressFormat.PNG, 100, stream) - it.recycle() // Release the bitmap resources - return stream.toByteArray() - } - */ - val bitmap = createVideoThumbnail(file.absolutePath); - if (bitmap != null){ - val stream = ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); - bitmap.recycle(); - return stream.toByteArray(); + fileInfoList.add(mapOf("url" to url, "mimeType" to mimeType)) } } - } catch (e: Exception) { - // e.printStackTrace() } - return ByteArray(0) - } + "WHATSAPP", "WHATSAPP4B" -> { + val docUri = if (appType == "WHATSAPP") { + Common.whatsappMediaQuery(contentResolver) + } else { + Common.whatsappBusinessMediaQuery(contentResolver) + } - fun processFiles(files: Array?, source: String) { - files?.forEach { file -> - val fileInfo = mutableMapOf() - fileInfo["name"] = file.name - fileInfo["path"] = file.absolutePath - fileInfo["size"] = file.length() - fileInfo["format"] = getFileFormat(file.name) - fileInfo["source"] = source - fileInfo["mediaByte"] = ByteArray(0) - /* - fileInfo["mediaByte"] = getMediaByte(file, fileInfo["format"] as String) - */ - - statusFilesInfo.add(fileInfo) + val projection = arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_MIME_TYPE + ) + val validDocUri = docUri ?: return fileInfoList + val cursor = contentResolver.query(validDocUri, projection, null, null, null) + cursor?.use { cursorInstance -> + while (cursorInstance.moveToNext()) { + val documentId = cursorInstance.getString(cursorInstance.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) + val mimeType = cursorInstance.getString(cursorInstance.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE)) ?: "application/octet-stream" + val url = "http://localhost:8080/files/$documentId" + + fileInfoList.add(mapOf("url" to url, "mimeType" to mimeType)) + } + } } + else -> Log.d("FileQuery", "Invalid app type.") } - if(appType == "SAVED"){ - Common.SAVEDSTATUSES?.let { processFiles(it.listFiles(FileFilter { file -> file.isFile && file.canRead() }), "SAVED") } - } - else if(appType == "WHATSAPP"){ - Common.WHATSAPP?.let { processFiles(it.listFiles(FileFilter { file -> file.isFile && file.canRead() }), "WHATSAPP") } - } - else if(appType == "WHATSAPP4B"){ - Common.WHATSAPP4B?.let { processFiles(it.listFiles(FileFilter { file -> file.isFile && file.canRead() }), "WHATSAPP4B") } - } - else if(appType == "ALLWHATSAPP"){ - Common.WHATSAPP?.let { processFiles(it.listFiles(FileFilter { file -> file.isFile && file.canRead() }), "WHATSAPP") } - Common.WHATSAPP4B?.let { processFiles(it.listFiles(FileFilter { file -> file.isFile && file.canRead() }), "WHATSAPP4B") } - } - /* - else if(appType == "SAVEDWHATSAPP"){ - Common.SAVEDWHATSAPP?.let { processFiles(it.listFiles(FileFilter { file -> file.isFile && file.canRead() }), "whatsapp") } - } - else if(appType == "SAVEDWHATSAPP4B"){ - Common.SAVEDWHATSAPP4B?.let { processFiles(it.listFiles(FileFilter { file -> file.isFile && file.canRead() }), "whatsapp4b") } - } - */ - return statusFilesInfo + return fileInfoList } - // ignore, let try the new one - /* - private fun getStatusFilesInfo(): List> { - val statusFilesInfo = mutableListOf>() - - val whatsapp = mutableListOf>() - Common.WHATSAPP?.let { - val files = it.listFiles(FileFilter { file -> - file.isFile && file.canRead() - }) - files?.forEach { file -> - val fileInfo = mutableMapOf() - fileInfo["name"] = file.name - fileInfo["path"] = file.absolutePath - fileInfo["size"] = file.length() - fileInfo["format"] = getFileFormat(file.name) - fileInfo["source"] = "whatsapp" - whatsapp.add(fileInfo) - } - } - val whatsapp4b = mutableListOf>() - Common.WHATSAPP4B?.let { - val files = it.listFiles(FileFilter { file -> - file.isFile && file.canRead() - }) - files?.forEach { file -> - val fileInfo = mutableMapOf() - fileInfo["name"] = file.name - fileInfo["path"] = file.absolutePath - fileInfo["size"] = file.length() - fileInfo["format"] = getFileFormat(file.name) - fileInfo["source"] = "whatsapp4b" - whatsapp4b.add(fileInfo) - } - } - val whatsapp4b = mutableListOf>() - Common.WHATSAPP4B?.let { - val files = it.listFiles(FileFilter { file -> - file.isFile && file.canRead() - }) - files?.forEach { file -> - val fileInfo = mutableMapOf() - fileInfo["name"] = file.name - fileInfo["path"] = file.absolutePath - fileInfo["size"] = file.length() - fileInfo["format"] = getFileFormat(file.name) - fileInfo["source"] = "whatsapp4b" - whatsapp4b.add(fileInfo) - } - } - val savedstatus = mutableListOf>() - Common.SAVEDSTATUSES?.let { - val files = it.listFiles(FileFilter { file -> - file.isFile && file.canRead() - }) - files?.forEach { file -> - val fileInfo = mutableMapOf() - fileInfo["name"] = file.name - fileInfo["path"] = file.absolutePath - fileInfo["size"] = file.length() - fileInfo["format"] = getFileFormat(file.name) - fileInfo["source"] = "whatsapp4b" - whatsapp4b.add(fileInfo) - } - } - statusFilesInfo.addAll(whatsapp) - statusFilesInfo.addAll(whatsapp4b) - statusFilesInfo.addAll(savedstatus) - return statusFilesInfo - } - */ - - private fun checkStoragePermission(): Boolean { - val hasPermission = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Environment.isExternalStorageManager() + private suspend fun downloadAndSaveFile(fileUrl: String): String { + // Extract fileId from fileUrl + val fileId = try { + val pathSegments = Uri.parse(fileUrl).pathSegments + if (pathSegments.isNotEmpty()) { + pathSegments.last().toLongOrNull() } else { - ContextCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED + null } - // Log.d(TAG, "Has storage permission: $hasPermission") - return hasPermission - } + } catch (e: Exception) { + null + } ?: return "Invalid File ID" - private fun requestStoragePermission(): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:" + packageName)) - activity.startActivityForResult(intent, APP_STORAGE_ACCESS_REQUEST_CODE) - return true - } else { - // For versions below Android 10, request WRITE_EXTERNAL_STORAGE permission - ActivityCompat.requestPermissions(this.activity,arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),APP_STORAGE_ACCESS_REQUEST_CODE) - return true - } - } + return withContext(Dispatchers.IO) { + val client = OkHttpClient.Builder().readTimeout(60, TimeUnit.SECONDS).build() + val request = Request.Builder() + .url(fileUrl) + .build() - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == APP_STORAGE_ACCESS_REQUEST_CODE) { - if (resultCode == RESULT_OK && Environment.isExternalStorageManager()) { - // Log.d(TAG, "Storage permission granted") - } else { - // Log.d(TAG, "Storage permission denied") - } - } - } + try { + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + return@withContext "Download Failed" + } - private fun saveStatus(sourceFilePath: String): String { - val sourceFile = File(sourceFilePath) - val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) - intent.data = Uri.fromFile(Common.SAVEDSTATUSES) + val mimeType = response.header("Content-Type") + val fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) + val contentResolver = applicationContext.contentResolver - return if (sourceFile.exists()) { - try { - val galleryDirectory = Common.SAVEDSTATUSES + val fileName = "mediasaver_$fileId.$fileExtension" - if (!galleryDirectory.exists()) { - galleryDirectory.mkdirs() - } + // Check if file already exists in MediaStore + val cursor = Common.savedMediasQuery(contentResolver) + val fileExists = cursor?.use { + val displayNameColumn = it.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME) + while (it.moveToNext()) { + val existingFileName = it.getString(displayNameColumn) + if (existingFileName == fileName) { + return@withContext "File already exists" + } + } + false + } ?: false + + if (fileExists) { + return@withContext "File already exists" + } - val originalFileName = sourceFile.name - val originalExtension = getExtension(sourceFile) + // Prepare ContentValues for MediaStore + val values = ContentValues().apply { + put(MediaStore.Images.Media.DISPLAY_NAME, fileName) + put(MediaStore.Images.Media.MIME_TYPE, mimeType) + put(MediaStore.Images.Media.IS_PENDING, 1) + } - val newImageFile = File(galleryDirectory, originalFileName) + // Insert a new entry in MediaStore + val savedUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) + ?: return@withContext "Failed to create MediaStore entry" - // If a file with the same name already exists, return "Already Saved" - if (newImageFile.exists()) { - return "Already Saved" - } + // Open streams for downloading and writing to MediaStore + val inputStream: InputStream? = response.body?.byteStream() + val outputStream = contentResolver.openOutputStream(savedUri) ?: return@withContext "Failed to save file" - FileInputStream(sourceFile).use { inputStream -> - FileOutputStream(newImageFile).use { outputStream -> - val buffer = ByteArray(4 * 1024) - var bytesRead: Int - while (inputStream.read(buffer).also { bytesRead = it } >= 0) { - outputStream.write(buffer, 0, bytesRead) + val buffer = ByteArray(2048) + var bytesRead: Int + + inputStream.use { input -> + outputStream.use { output -> + while (input?.read(buffer).also { bytesRead = it ?: -1 } != -1) { + output.write(buffer, 0, bytesRead) + } } } - } - applicationContext.sendBroadcast(intent) + // Mark the file as ready by clearing IS_PENDING + values.clear() + values.put(MediaStore.Images.Media.IS_PENDING, 0) + contentResolver.update(savedUri, values, null, null) - // File saved successfully - "Status Saved" - } catch (e: IOException) { - // e.printStackTrace() - // Error saving file - "Not Saved" + return@withContext when { + mimeType?.startsWith("video/") == true -> "Video saved" + mimeType?.startsWith("image/") == true -> "Image saved" + else -> "File saved" + } } - } else { - // Source file doesn't exist - "Not Saved" + } catch (e: IOException) { + e.printStackTrace() + return@withContext "IO Exception, Try again!" + } catch (e: Exception) { + e.printStackTrace() + return@withContext "Unexpected Error: ${e.message}" + } } } - private fun deleteStatus(filePath: String): String { - val folder = Common.SAVEDSTATUSES - val file = File(filePath) + // PERMISSION HANDLING - return try { - if (file.exists() && file.delete()) { - "deleted" - }else{ - "not deleted" + private fun hasFolderAccess(): Boolean { + val isPermit = Common.hasPermission(context) + return isPermit + } + + private fun requestAccessToFolder() { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + startActivityForResult(intent, Common.REQUEST_CODE_WHATSAPP_FOLDER) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == Activity.RESULT_OK) { + val uri = data?.data ?: return + Log.d("uri", " $uri") + + if (requestCode == Common.REQUEST_CODE_WHATSAPP_FOLDER && uri.toString() == "content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fmedia") { + Common.updatePermission(true, context) + } else { + Toast.makeText(applicationContext, "Please select the Media folder", Toast.LENGTH_SHORT).show() + requestAccessToFolder() } - } catch (e: SecurityException) { - // e.printStackTrace() - "not deleted" } } - private fun getExtension(file: File): String { - val name = file.name - val lastDotIndex = name.lastIndexOf('.') - return if (lastDotIndex == -1) { - "jpg" - } else { - name.substring(lastDotIndex + 1).toLowerCase() + + // OTHERS + + private fun copyText(): String{ + val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + if (clipboardManager.hasPrimaryClip()) { + val clipData: ClipData? = clipboardManager.primaryClip + if (clipData != null && clipData.itemCount > 0) { + val item = clipData.getItemAt(0) + return item.text.toString() + } } + return "" } - private suspend fun downloadAndSaveFile(fileUrl: String, fileId: String): String { - return withContext(Dispatchers.IO) { - val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) - intent.data = Uri.fromFile(Common.SAVEDSTATUSES) - val client = OkHttpClient.Builder().readTimeout(60, TimeUnit.SECONDS).build() - val request = Request.Builder() - .url(fileUrl) - .build() - - try { - client.newCall(request).execute().use { response -> - if (!response.isSuccessful) { - return@withContext "Download Failed" - } - val mimeType = response.header("Content-Type") - val fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) + private fun shareApp(): Boolean{ + // Share intent + val shareIntent = Intent(Intent.ACTION_SEND) + shareIntent.type = "text/plain" + + // Set the subject and message + val shareSubject = "Check out this free Media Saver" + val shareMessage = "$shareSubject\n\nNo Ads, No Cost—Download Videos and Photos From Instagram, Facebook, and TikTok for Free!\n\nGet the app: https://play.google.com/store/apps/details?id=com.blackstackhub.mediasaver" + + // shareIntent.putExtra(Intent.EXTRA_SUBJECT, shareSubject) + shareIntent.putExtra(Intent.EXTRA_TEXT, shareMessage) - val galleryDirectory = Common.SAVEDSTATUSES - if (!galleryDirectory.exists()) { - galleryDirectory.mkdirs() - } + try { + startActivity(Intent.createChooser(shareIntent, "Share via")) + } catch (e: Exception) { + // Handle exceptions + // e.printStackTrace() + // You might want to return an error message or handle it differently + } + return true + } - val fileName = "mediasaver_$fileId.$fileExtension" - val saveFile = File(galleryDirectory, fileName) + private fun sendEmail(): Boolean{ + val emailIntent = Intent(Intent.ACTION_SENDTO) - if (saveFile.exists()) { - return@withContext "Already Saved" - } + val packageInfo = applicationContext.packageManager.getPackageInfo(context.packageName, 0) + val appVersion = packageInfo.versionName - val inputStream: InputStream? = response.body?.byteStream() - val outputStream = FileOutputStream(saveFile) - val buffer = ByteArray(2048) - var bytesRead: Int + val locale = Locale.getDefault() + val country = locale.displayCountry - inputStream.use { input -> - outputStream.use { output -> - while (input?.read(buffer).also { bytesRead = it ?: -1 } != -1) { - output.write(buffer, 0, bytesRead) - } - } - } - applicationContext.sendBroadcast(intent) + val email = "devfemibadmus@gmail.com" + val subject = "Request a new feature for Media-Saver" + + val preBody = "Version: ${appVersion}\nCountry: $country\n\nI want to request feature for..." + val mailtoLink = "mailto:$email?subject=$subject&body=$preBody" - return@withContext when { - mimeType?.startsWith("video/") == true -> "Video saved" - mimeType?.startsWith("image/") == true -> "Image saved" - else -> "File saved" - } - } - } catch (e: SecurityException) { - e.printStackTrace() - return@withContext "Restart app and give permission." - } catch (e: IOException) { - e.printStackTrace() - return@withContext "IO Exception, Try again!" - } - } + // Set the email address + emailIntent.data = Uri.parse(mailtoLink) + + // Check if there is a package that can handle the intent + try { + startActivity(emailIntent) + } catch (e: Exception) { + // If no email app is available, open a web browser + val webIntent = Intent(Intent.ACTION_VIEW) + webIntent.data = Uri.parse("https://github.com/devfemibadmus/Media-Saver") + startActivity(webIntent) + } + return true + } + + private fun launchurl(url: String): Boolean{ + val webIntent = Intent(Intent.ACTION_VIEW) + webIntent.data = Uri.parse(url) + startActivity(webIntent) + return true } } +