Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(android): UI Blocking code in Android when fetching JS Bundle && Add kotlin config in to 0.71 sample #122

Merged
merged 3 commits into from
Feb 19, 2025
Merged
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
1 change: 1 addition & 0 deletions examples/v0.71.19/android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"

import com.android.build.OutputFile
Expand Down
2 changes: 2 additions & 0 deletions examples/v0.71.19/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ buildscript {

// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
kotlinVersion = "1.9.24"
}
repositories {
google()
Expand All @@ -17,5 +18,6 @@ buildscript {
dependencies {
classpath("com.android.tools.build:gradle:7.3.1")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
}
}
4 changes: 4 additions & 0 deletions packages/react-native/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ buildscript {
classpath "com.android.tools.build:gradle:7.2.1"
// noinspection DifferentKotlinGradleVersion
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

}
}

Expand Down Expand Up @@ -122,6 +123,9 @@ dependencies {
implementation 'com.facebook.react:react-native:+'
}
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
}

if (isNewArchitectureEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ReactShadowNode
import com.facebook.react.uimanager.ViewManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.net.HttpURLConnection
import java.net.URL
Expand All @@ -28,9 +30,9 @@ class HotUpdater : ReactPackage {
context: Context,
basePath: String,
): String {
val documentsDir = context.getExternalFilesDir(null)?.absolutePath ?: context.filesDir.absolutePath
val documentsDir =
context.getExternalFilesDir(null)?.absolutePath ?: context.filesDir.absolutePath
val separator = if (basePath.startsWith("/")) "" else "/"

return "$documentsDir$separator$basePath"
}

Expand Down Expand Up @@ -65,12 +67,11 @@ class HotUpdater : ReactPackage {
}

val reactIntegrationManager = ReactIntegrationManager(context)

val activity: Activity? = getCurrentActivity(context)
val reactApplication: ReactApplication = reactIntegrationManager.getReactApplication(activity?.application)
val bundleURL = getJSBundleFile(context)

reactIntegrationManager.setJSBundle(reactApplication, bundleURL)
val reactApplication: ReactApplication =
reactIntegrationManager.getReactApplication(activity?.application)
val newBundleURL = getJSBundleFile(context)
reactIntegrationManager.setJSBundle(reactApplication, newBundleURL)
}

private fun extractZipFileAtPath(
Expand Down Expand Up @@ -106,19 +107,17 @@ class HotUpdater : ReactPackage {

fun reload(context: Context) {
val reactIntegrationManager = ReactIntegrationManager(context)

val activity: Activity? = getCurrentActivity(context)
val reactApplication: ReactApplication = reactIntegrationManager.getReactApplication(activity?.application)
val reactApplication: ReactApplication =
reactIntegrationManager.getReactApplication(activity?.application)
val bundleURL = getJSBundleFile(context)

reactIntegrationManager.setJSBundle(reactApplication, bundleURL)

Handler(Looper.getMainLooper()).post {
reactIntegrationManager.reload(reactApplication)
}
}

public fun getJSBundleFile(context: Context): String {
fun getJSBundleFile(context: Context): String {
val sharedPreferences =
context.getSharedPreferences("HotUpdaterPrefs", Context.MODE_PRIVATE)
val urlString = sharedPreferences.getString("HotUpdaterBundleURL", null)
Expand All @@ -135,7 +134,7 @@ class HotUpdater : ReactPackage {
return urlString
}

fun updateBundle(
suspend fun updateBundle(
context: Context,
bundleId: String,
zipUrl: String?,
Expand All @@ -148,66 +147,81 @@ class HotUpdater : ReactPackage {
}

val downloadUrl = URL(zipUrl)

val basePath = stripPrefixFromPath(bundleId, downloadUrl.path)
val path = convertFileSystemPathFromBasePath(context, basePath)

var connection: HttpURLConnection? = null
try {
connection = downloadUrl.openConnection() as HttpURLConnection
connection.connect()

val totalSize = connection.contentLength
if (totalSize <= 0) {
Log.d("HotUpdater", "Invalid content length: $totalSize")
return false
}

val file = File(path)
file.parentFile?.mkdirs()
val isSuccess =
withContext(Dispatchers.IO) {
val conn =
try {
downloadUrl.openConnection() as HttpURLConnection
} catch (e: Exception) {
Log.d("HotUpdater", "Failed to open connection: ${e.message}")
return@withContext false
}

connection.inputStream.use { input ->
file.outputStream().use { output ->
val buffer = ByteArray(8 * 1024)
var bytesRead: Int
var totalRead = 0L
try {
conn.connect()
val totalSize = conn.contentLength
if (totalSize <= 0) {
Log.d("HotUpdater", "Invalid content length: $totalSize")
return@withContext false
}

while (input.read(buffer).also { bytesRead = it } != -1) {
output.write(buffer, 0, bytesRead)
totalRead += bytesRead
val progress = (totalRead.toDouble() / totalSize)
progressCallback.invoke(progress)
val file = File(path)
file.parentFile?.mkdirs()

conn.inputStream.use { input ->
file.outputStream().use { output ->
val buffer = ByteArray(8 * 1024)
var bytesRead: Int
var totalRead = 0L
var lastProgressTime = System.currentTimeMillis()

while (input.read(buffer).also { bytesRead = it } != -1) {
output.write(buffer, 0, bytesRead)
totalRead += bytesRead
val currentTime = System.currentTimeMillis()
if (currentTime - lastProgressTime >= 100) { // Check every 100ms
val progress = totalRead.toDouble() / totalSize
progressCallback.invoke(progress)
lastProgressTime = currentTime
}
}
// Send final progress (100%) after download completes
progressCallback.invoke(1.0)
}
}
} catch (e: Exception) {
Log.d("HotUpdater", "Failed to download data from URL: $zipUrl, Error: ${e.message}")
return@withContext false
} finally {
conn.disconnect()
}
}
} catch (e: Exception) {
Log.d("HotUpdater", "Failed to download data from URL: $zipUrl, Error: ${e.message}")
return false
} finally {
connection?.disconnect()
}

val extractedPath = File(path).parentFile?.path ?: return false
val extractedPath = File(path).parentFile?.path ?: return@withContext false

if (!extractZipFileAtPath(path, extractedPath)) {
Log.d("HotUpdater", "Failed to extract zip file.")
return false
}
if (!extractZipFileAtPath(path, extractedPath)) {
Log.d("HotUpdater", "Failed to extract zip file.")
return@withContext false
}

val extractedDirectory = File(extractedPath)
val indexFile = extractedDirectory.walk().find { it.name == "index.android.bundle" }
val extractedDirectory = File(extractedPath)
val indexFile = extractedDirectory.walk().find { it.name == "index.android.bundle" }

if (indexFile != null) {
val bundlePath = indexFile.path
Log.d("HotUpdater", "Setting bundle URL: $bundlePath")
setBundleURL(context, bundlePath)
} else {
Log.d("HotUpdater", "index.android.bundle not found.")
return false
}
if (indexFile != null) {
val bundlePath = indexFile.path
Log.d("HotUpdater", "Setting bundle URL: $bundlePath")
setBundleURL(context, bundlePath)
} else {
Log.d("HotUpdater", "index.android.bundle not found.")
return@withContext false
}

Log.d("HotUpdater", "Downloaded and extracted file successfully.")
return true
Log.d("HotUpdater", "Downloaded and extracted file successfully.")
true
}
return isSuccess
}
}
}
35 changes: 23 additions & 12 deletions packages/react-native/android/src/newarch/HotUpdaterModule.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.hotupdater

import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.WritableNativeMap
import com.facebook.react.modules.core.DeviceEventManagerModule
import kotlinx.coroutines.launch

class HotUpdaterModule internal constructor(
context: ReactApplicationContext,
Expand All @@ -29,18 +32,26 @@ class HotUpdaterModule internal constructor(
zipUrl: String?,
promise: Promise,
) {
val isSuccess =
HotUpdater.updateBundle(mReactApplicationContext, bundleId, zipUrl) { progress ->
val params =
WritableNativeMap().apply {
putDouble("progress", progress)
}

this.mReactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("onProgress", params)
}
promise.resolve(isSuccess)
// Use lifecycleScope when currentActivity is FragmentActivity
(currentActivity as? FragmentActivity)?.lifecycleScope?.launch {
val isSuccess =
HotUpdater.updateBundle(
mReactApplicationContext,
bundleId,
zipUrl,
) { progress ->
val params =
WritableNativeMap().apply {
putDouble("progress", progress)
}

this@HotUpdaterModule
.mReactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("onProgress", params)
}
promise.resolve(isSuccess)
}
}

override fun addListener(eventName: String?) {
Expand Down
35 changes: 23 additions & 12 deletions packages/react-native/android/src/oldarch/HotUpdaterModule.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.hotupdater

import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.WritableNativeMap
import com.facebook.react.modules.core.DeviceEventManagerModule
import kotlinx.coroutines.launch

class HotUpdaterModule internal constructor(
context: ReactApplicationContext,
Expand All @@ -29,18 +32,26 @@ class HotUpdaterModule internal constructor(
zipUrl: String?,
promise: Promise,
) {
val isSuccess =
HotUpdater.updateBundle(mReactApplicationContext, bundleId, zipUrl) { progress ->
val params =
WritableNativeMap().apply {
putDouble("progress", progress)
}

this.mReactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("onProgress", params)
}
promise.resolve(isSuccess)
// Use lifecycleScope when currentActivity is FragmentActivity
(currentActivity as? FragmentActivity)?.lifecycleScope?.launch {
val isSuccess =
HotUpdater.updateBundle(
mReactApplicationContext,
bundleId,
zipUrl,
) { progress ->
val params =
WritableNativeMap().apply {
putDouble("progress", progress)
}

this@HotUpdaterModule
.mReactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("onProgress", params)
}
promise.resolve(isSuccess)
}
}

companion object {
Expand Down