Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 19 additions & 44 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,48 +1,35 @@
buildscript {
ext {
kotlinVersion = '1.6.20'
kotlinVersion = '2.2.20'
buildToolsVersion = '29.0.2'
compileSdkVersion = 31 // this helps us use the latest Worker version
targetSdkVersion = 29
minSdkVersion = 18
minSdkVersion = 29
compileSdkVersion = 35
targetSdkVersion = 35
}
ext.detoxKotlinVersion = ext.kotlinVersion

repositories {
mavenCentral()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.4'
classpath 'com.android.tools.build:gradle:8.13.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

def DEFAULT_COMPILE_SDK_VERSION = 28
def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3"
def DEFAULT_TARGET_SDK_VERSION = 28

def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
def safeExtGet(prop) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : ext.get(prop)
}

android {
buildFeatures {
/*
This is a replacement for the deprecated 'kotlin-android-extensions' library. More
information can be found here: https://developer.android.com/topic/libraries/view-binding/migration
*/
viewBinding = true
}
compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION)
buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION)
compileSdkVersion safeExtGet('compileSdkVersion')
buildToolsVersion safeExtGet('buildToolsVersion')

defaultConfig {
minSdkVersion 24
targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION)
minSdkVersion safeExtGet('minSdkVersion')
targetSdkVersion safeExtGet('targetSdkVersion')
versionCode 1
versionName "1.0"
ndk {
Expand All @@ -65,25 +52,13 @@ repositories {
mavenCentral()
}

def _ext = ext

def _kotlinVersion = _ext.has('detoxKotlinVersion') ? _ext.detoxKotlinVersion : '1.3.10'
def _kotlinStdlib = _ext.has('detoxKotlinStdlib') ? _ext.detoxKotlinStdlib : 'kotlin-stdlib-jdk8'

dependencies {
implementation "androidx.core:core-ktx:1.7.0"

implementation "androidx.core:core-ktx:1.17.0"
implementation 'com.facebook.react:react-native:+'

implementation "org.jetbrains.kotlin:$_kotlinStdlib:$_kotlinVersion"

implementation("com.squareup.okhttp3:okhttp:4.10.0")

implementation 'com.google.code.gson:gson:2.8.9'

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'

implementation "androidx.work:work-runtime-ktx:2.8.1"
}
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
implementation("com.squareup.okhttp3:okhttp:5.2.1")
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2'
implementation "androidx.work:work-runtime-ktx:2.10.5"
implementation 'com.google.code.gson:gson:2.13.2'
}
67 changes: 67 additions & 0 deletions android/src/main/java/com/vydia/RNUploader2/EventReporter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.vydia.RNUploader2

import android.util.Log
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

// Sends events to React Native
class EventReporter {
companion object {
private const val TAG = "UploadReceiver"
fun cancelled(uploadId: String) =
sendEvent("cancelled", Arguments.createMap().apply {
putString("id", uploadId)
})

fun error(uploadId: String, exception: Throwable) =
sendEvent("error", Arguments.createMap().apply {
putString("id", uploadId)
putString("error", exception.message ?: "Unknown exception")
})

// TODO expose via JS
fun globalError(origin: String, exception: Throwable) =
sendEvent("globalError", Arguments.createMap().apply {
putString("origin", origin)
putString("error", exception.message ?: "Unknown exception")
})

fun success(uploadId: String, response: UploadResponse) =
CoroutineScope(Dispatchers.IO).launch {
sendEvent("completed", Arguments.createMap().apply {
putString("id", uploadId)
putInt("responseCode", response.statusCode)
putString("responseBody", response.body)
putMap("responseHeaders", Arguments.makeNativeMap(response.headers))
})
}

fun progress(uploadId: String) =
sendEvent("progress", Arguments.createMap().apply {
putString("id", uploadId)
putDouble("progress", UploadQueue.progressPercentage().toDouble())
})

fun notification() = sendEvent("notification")

/** Sends an event to the JS module */
private fun sendEvent(eventName: String, params: WritableMap = Arguments.createMap()) {
val reactContext = UploaderModule2.reactContext ?: return

// Right after JS reloads, react instance might not be available yet
if (!reactContext.hasActiveReactInstance()) return

try {
val jsModule = reactContext.getJSModule(RCTDeviceEventEmitter::class.java)
jsModule.emit("RNFileUploader-$eventName", params)
} catch (exc: Throwable) {
Log.e(TAG, "sendEvent() failed", exc)
}
}
}

}
31 changes: 31 additions & 0 deletions android/src/main/java/com/vydia/RNUploader2/NotificationConfigs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.vydia.RNUploader2

import com.facebook.react.bridge.ReadableMap


object NotificationConfigs {
var id: Int = 0
private set
var title: String = "Uploading files"
private set
var titleNoInternet: String = "Waiting for internet connection"
private set
var titleNoWifi: String = "Waiting for WiFi connection"
private set
var channel: String = "File Uploads"
private set

fun update(opts: ReadableMap) {
id = opts.getString("notificationId")?.hashCode()
?: throw MissingOptionException("notificationId")
title = opts.getString("notificationTitle")
?: throw MissingOptionException("notificationTitle")
titleNoInternet = opts.getString("notificationTitleNoInternet")
?: throw MissingOptionException("notificationTitleNoInternet")
titleNoWifi = opts.getString("notificationTitleNoWifi")
?: throw MissingOptionException("notificationTitleNoWifi")
channel = opts.getString("notificationChannel")
?: throw MissingOptionException("notificationChannel")
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.vydia.RNUploader2

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent

// there's no way to directly open the app from the notification without reloading it,
// so we use a BroadcastReceiver to listen to the notification intent
class NotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
context ?: return
val packageName = context.packageName
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
context.startActivity(launchIntent)
EventReporter.notification()
}
}
40 changes: 40 additions & 0 deletions android/src/main/java/com/vydia/RNUploader2/Upload.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.vydia.RNUploader2

import com.facebook.react.bridge.ReadableMap
import java.util.UUID

// Data model of a single upload
// Can be created from RN's ReadableMap
// Can be used for JSON deserialization
data class Upload(
val id: String,
val url: String,
val path: String,
val method: String,
val maxRetries: Int,
val wifiOnly: Boolean,
val headers: Map<String, String>,
) {
// Progress tracking properties
var bytesUploaded: Long = 0L
var size: Long = 0L

companion object {
fun fromRawOptions(map: ReadableMap) = Upload(
id = map.getString("customUploadId") ?: UUID.randomUUID().toString(),
url = map.getString(Upload::url.name) ?: throw MissingOptionException(Upload::url.name),
path = map.getString(Upload::path.name) ?: throw MissingOptionException(Upload::path.name),
method = map.getString(Upload::method.name) ?: "POST",
maxRetries = if (map.hasKey(Upload::maxRetries.name)) map.getInt(Upload::maxRetries.name) else 5,
wifiOnly = if (map.hasKey(Upload::wifiOnly.name)) map.getBoolean(Upload::wifiOnly.name) else false,
headers = map.getMap(Upload::headers.name).let { headers ->
if (headers == null) return@let mapOf()
val map = mutableMapOf<String, String>()
for (entry in headers.entryIterator) {
map[entry.key] = entry.value.toString()
}
return@let map
},
)
}
}
87 changes: 87 additions & 0 deletions android/src/main/java/com/vydia/RNUploader2/UploadQueue.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.vydia.RNUploader2

import java.io.File
import java.io.FileNotFoundException

object UploadQueue {
private val queue = ArrayDeque<Upload>()

/**
* Keeps track of the total bytes of completed uploads to report overall progress correctly.
*/
private var completedBytes = 0L

/**
* Returns the overall progress percentage of all uploads in the queue.
* 0 - 100
*/
@Synchronized
fun progressPercentage(): Float {
if (queue.isEmpty()) return 0f

val totalBytes = queue.sumOf { it.size } + completedBytes
if (totalBytes == 0L) return 0f
val uploadedBytes = (current()?.bytesUploaded ?: 0L) + completedBytes

return uploadedBytes.toFloat() / totalBytes.toFloat() * 100
}

@Synchronized
fun add(upload: Upload) {
if (queue.any { it.id == upload.id }) return

val file = File(upload.path)
if (file.exists())
upload.size = file.length()
else
throw FileNotFoundException("File at path ${upload.path} does not exist")

queue.add(upload)
}

@Synchronized
fun progress(bytesUploaded: Long) {
current()?.bytesUploaded = bytesUploaded
}

@Synchronized
fun complete() {
// Extract size immediately and allow upload object to be garbage collected
completedBytes += queue.removeFirst().size
}

@Synchronized
fun pop() = queue.removeFirst()

@Synchronized
fun cancel(uploadId: String) = queue.removeIf { it.id == uploadId }


@Synchronized
fun skipWifiOnly(): Boolean {
// Find index instead of element to avoid double search
val index = queue.indexOfFirst { !it.wifiOnly }
if (index == -1) return false
if (index == 0) return true // Already at front

// Remove by index (still O(n) but avoids the find step)
val upload = queue.removeAt(index)
queue.addFirst(upload)
return true
}

@Synchronized
fun isAllWifiOnly() = !queue.isEmpty() && queue.all { it.wifiOnly }

@Synchronized
fun current() = queue.firstOrNull()

@Synchronized
fun clear() {
queue.clear()
completedBytes = 0L
}

@Synchronized
fun isEmpty() = queue.isEmpty()
}
Loading
Loading