add files listing
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'expo-module-gradle-plugin'
|
||||
}
|
||||
|
||||
group = 'host.exp.exponent'
|
||||
version = '0.1.0'
|
||||
|
||||
android {
|
||||
namespace "expo.modules.downloaddetect"
|
||||
defaultConfig {
|
||||
versionCode 1
|
||||
versionName "0.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package expo.modules.downloaddetect
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
data class DetectedFile(
|
||||
val id: String,
|
||||
val uri: String,
|
||||
val name: String,
|
||||
val mimeType: String,
|
||||
val size: Long,
|
||||
val createdAt: Long
|
||||
)
|
||||
|
||||
class DownloadBroadcastReceiver(
|
||||
private val onFileDetected: (DetectedFile) -> Unit
|
||||
) : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != DownloadManager.ACTION_DOWNLOAD_COMPLETE) return
|
||||
|
||||
val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
||||
if (downloadId == -1L) return
|
||||
|
||||
val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||
|
||||
dm.query(query)?.use { cursor ->
|
||||
if (!cursor.moveToFirst()) return
|
||||
|
||||
val uriIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
|
||||
val titleIndex = cursor.getColumnIndex(DownloadManager.COLUMN_TITLE)
|
||||
val mimeTypeIndex = cursor.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE)
|
||||
val sizeIndex = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
|
||||
val dateIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP)
|
||||
|
||||
val uri = if (uriIndex >= 0) cursor.getString(uriIndex) ?: "" else ""
|
||||
val title = if (titleIndex >= 0) cursor.getString(titleIndex) ?: "unknown" else "unknown"
|
||||
val mimeType = if (mimeTypeIndex >= 0) cursor.getString(mimeTypeIndex) ?: "application/octet-stream" else "application/octet-stream"
|
||||
val size = if (sizeIndex >= 0) cursor.getLong(sizeIndex) else 0L
|
||||
val date = if (dateIndex >= 0) cursor.getLong(dateIndex) else System.currentTimeMillis()
|
||||
|
||||
// Extract filename from URI
|
||||
val name = extractFileName(uri, title)
|
||||
|
||||
onFileDetected(DetectedFile(
|
||||
id = "download_$downloadId",
|
||||
uri = uri,
|
||||
name = name,
|
||||
mimeType = mimeType,
|
||||
size = size,
|
||||
createdAt = date
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractFileName(uri: String, fallback: String): String {
|
||||
if (uri.isNotEmpty()) {
|
||||
// content://media/external/downloads/123 or file:///storage/...
|
||||
val path = uri.substringAfterLast("/")
|
||||
if (path.isNotEmpty() && path.all { it.isDigit() }.not()) {
|
||||
return decodeFileName(path)
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
private fun decodeFileName(encoded: String): String {
|
||||
return try {
|
||||
java.net.URLDecoder.decode(encoded, "UTF-8")
|
||||
} catch (_: Exception) {
|
||||
encoded
|
||||
}
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
package expo.modules.downloaddetect
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.Context
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.os.bundleOf
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class ExpoDownloadDetectModule : Module() {
|
||||
|
||||
private var broadcastReceiver: DownloadBroadcastReceiver? = null
|
||||
private var contentObserver: MediaStoreObserver? = null
|
||||
private var isWatching = false
|
||||
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("ExpoDownloadDetect")
|
||||
|
||||
Events("onNewFile")
|
||||
|
||||
Function("startWatching") {
|
||||
if (!isWatching) startObserving()
|
||||
null
|
||||
}
|
||||
|
||||
Function("stopWatching") {
|
||||
stopObserving()
|
||||
}
|
||||
|
||||
AsyncFunction("getRecentDownloads") {
|
||||
queryRecentFiles()
|
||||
}
|
||||
}
|
||||
|
||||
private val context: Context
|
||||
get() = requireNotNull(appContext.reactContext)
|
||||
|
||||
private fun startObserving() {
|
||||
val ctx = context
|
||||
|
||||
// Register BroadcastReceiver for DownloadManager
|
||||
broadcastReceiver = DownloadBroadcastReceiver { event ->
|
||||
sendEvent("onNewFile", bundleOf(
|
||||
"id" to event.id,
|
||||
"uri" to event.uri,
|
||||
"name" to event.name,
|
||||
"mimeType" to event.mimeType,
|
||||
"size" to event.size,
|
||||
"createdAt" to event.createdAt,
|
||||
"source" to "download"
|
||||
))
|
||||
}
|
||||
|
||||
val intentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
ctx.registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
|
||||
|
||||
// Register ContentObserver on MediaStore
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
contentObserver = MediaStoreObserver(handler, ctx) { newFiles ->
|
||||
for (file in newFiles) {
|
||||
sendEvent("onNewFile", bundleOf(
|
||||
"id" to file.id,
|
||||
"uri" to file.uri,
|
||||
"name" to file.name,
|
||||
"mimeType" to file.mimeType,
|
||||
"size" to file.size,
|
||||
"createdAt" to file.createdAt,
|
||||
"source" to "mediastore"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
val uri = MediaStore.Files.getContentUri("external")
|
||||
ctx.contentResolver.registerContentObserver(uri, true, contentObserver!!)
|
||||
|
||||
// Scan existing files on startup
|
||||
val existingFiles = queryRecentFiles()
|
||||
for (file in existingFiles) {
|
||||
sendEvent("onNewFile", bundleOf(
|
||||
"id" to file.id,
|
||||
"uri" to file.uri,
|
||||
"name" to file.name,
|
||||
"mimeType" to file.mimeType,
|
||||
"size" to file.size,
|
||||
"createdAt" to file.createdAt,
|
||||
"source" to "startup"
|
||||
))
|
||||
}
|
||||
|
||||
isWatching = true
|
||||
}
|
||||
|
||||
private fun stopObserving() {
|
||||
val ctx = context
|
||||
|
||||
broadcastReceiver?.let {
|
||||
try {
|
||||
ctx.unregisterReceiver(it)
|
||||
} catch (_: Exception) {}
|
||||
broadcastReceiver = null
|
||||
}
|
||||
|
||||
contentObserver?.let {
|
||||
ctx.contentResolver.unregisterContentObserver(it)
|
||||
contentObserver = null
|
||||
}
|
||||
|
||||
isWatching = false
|
||||
}
|
||||
|
||||
private fun queryRecentFiles(): List<DetectedFile> {
|
||||
val files = mutableListOf<DetectedFile>()
|
||||
val projection = arrayOf(
|
||||
MediaStore.Files.FileColumns._ID,
|
||||
MediaStore.Files.FileColumns.DISPLAY_NAME,
|
||||
MediaStore.Files.FileColumns.MIME_TYPE,
|
||||
MediaStore.Files.FileColumns.SIZE,
|
||||
MediaStore.Files.FileColumns.DATE_ADDED
|
||||
)
|
||||
|
||||
val selection = "${MediaStore.Files.FileColumns.SIZE} > 0"
|
||||
val sortOrder = "${MediaStore.Files.FileColumns.DATE_ADDED} DESC"
|
||||
val limit = 100
|
||||
|
||||
context.contentResolver.query(
|
||||
MediaStore.Files.getContentUri("external"),
|
||||
projection,
|
||||
selection,
|
||||
null,
|
||||
sortOrder
|
||||
)?.use { cursor ->
|
||||
val idCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
|
||||
val nameCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)
|
||||
val mimeCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MIME_TYPE)
|
||||
val sizeCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE)
|
||||
val dateCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_ADDED)
|
||||
|
||||
var count = 0
|
||||
while (cursor.moveToNext() && count < limit) {
|
||||
val id = cursor.getLong(idCol)
|
||||
val name = cursor.getString(nameCol) ?: continue
|
||||
val mimeType = cursor.getString(mimeCol) ?: "application/octet-stream"
|
||||
val size = cursor.getLong(sizeCol)
|
||||
val dateAdded = cursor.getLong(dateCol)
|
||||
|
||||
val uri = Uri.withAppendedPath(
|
||||
MediaStore.Files.getContentUri("external"),
|
||||
id.toString()
|
||||
).toString()
|
||||
|
||||
files.add(DetectedFile(
|
||||
id = "media_$id",
|
||||
uri = uri,
|
||||
name = name,
|
||||
mimeType = mimeType,
|
||||
size = size,
|
||||
createdAt = dateAdded * 1000L
|
||||
))
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package expo.modules.downloaddetect
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.database.ContentObserver
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.provider.MediaStore
|
||||
|
||||
class MediaStoreObserver(
|
||||
handler: Handler,
|
||||
private val context: Context,
|
||||
private val onNewFiles: (List<DetectedFile>) -> Unit
|
||||
) : ContentObserver(handler) {
|
||||
|
||||
private var lastScanTimestamp: Long = System.currentTimeMillis() / 1000L
|
||||
|
||||
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
||||
super.onChange(selfChange, uri)
|
||||
queryNewFiles()
|
||||
}
|
||||
|
||||
override fun onChange(selfChange: Boolean, uri: Uri?, flags: Int) {
|
||||
super.onChange(selfChange, uri, flags)
|
||||
queryNewFiles()
|
||||
}
|
||||
|
||||
private fun queryNewFiles() {
|
||||
val files = mutableListOf<DetectedFile>()
|
||||
val projection = arrayOf(
|
||||
MediaStore.Files.FileColumns._ID,
|
||||
MediaStore.Files.FileColumns.DISPLAY_NAME,
|
||||
MediaStore.Files.FileColumns.MIME_TYPE,
|
||||
MediaStore.Files.FileColumns.SIZE,
|
||||
MediaStore.Files.FileColumns.DATE_ADDED
|
||||
)
|
||||
|
||||
val selection = "${MediaStore.Files.FileColumns.DATE_ADDED} > ? AND ${MediaStore.Files.FileColumns.SIZE} > 0"
|
||||
val selectionArgs = arrayOf(lastScanTimestamp.toString())
|
||||
val sortOrder = "${MediaStore.Files.FileColumns.DATE_ADDED} DESC"
|
||||
|
||||
val resolver: ContentResolver = context.contentResolver
|
||||
var cursor: Cursor? = null
|
||||
|
||||
try {
|
||||
// Try MediaStore.Downloads first
|
||||
cursor = try {
|
||||
resolver.query(
|
||||
MediaStore.Downloads.getContentUri("external"),
|
||||
projection, selection, selectionArgs, sortOrder
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
// Fallback to MediaStore.Files if Downloads didn't work
|
||||
if (cursor == null || cursor.count == 0) {
|
||||
cursor?.close()
|
||||
cursor = resolver.query(
|
||||
MediaStore.Files.getContentUri("external"),
|
||||
projection, selection, selectionArgs, sortOrder
|
||||
)
|
||||
}
|
||||
|
||||
cursor?.use { c ->
|
||||
val idCol = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
|
||||
val nameCol = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)
|
||||
val mimeCol = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MIME_TYPE)
|
||||
val sizeCol = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE)
|
||||
val dateCol = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_ADDED)
|
||||
|
||||
while (c.moveToNext()) {
|
||||
val id = c.getLong(idCol)
|
||||
val name = c.getString(nameCol) ?: continue
|
||||
val mimeType = c.getString(mimeCol) ?: "application/octet-stream"
|
||||
val size = c.getLong(sizeCol)
|
||||
val dateAdded = c.getLong(dateCol)
|
||||
|
||||
val fileUri = Uri.withAppendedPath(
|
||||
MediaStore.Files.getContentUri("external"),
|
||||
id.toString()
|
||||
).toString()
|
||||
|
||||
files.add(DetectedFile(
|
||||
id = "media_$id",
|
||||
uri = fileUri,
|
||||
name = name,
|
||||
mimeType = mimeType,
|
||||
size = size,
|
||||
createdAt = dateAdded * 1000L
|
||||
))
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
} finally {
|
||||
cursor?.close()
|
||||
}
|
||||
|
||||
if (files.isNotEmpty()) {
|
||||
lastScanTimestamp = System.currentTimeMillis() / 1000L
|
||||
onNewFiles(files)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user