diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 142d919a7a18..33a12b929e98 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -22,6 +22,11 @@ android:theme="@style/AppTheme" tools:replace="android:supportsRtl"> + + + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['<%- project.name -%>_' + name] + } + + repositories { + google() + mavenCentral() + } + + dependencies { + // noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}" + } +} + +def reactNativeArchitectures() { + def value = rootProject.getProperties().get("reactNativeArchitectures") + return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] +} + +def isNewArchitectureEnabled() { + return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +if (isNewArchitectureEnabled()) { + apply plugin: "com.facebook.react" +} + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ReactNativeBackgroundTask_" + name]).toInteger() +} + +android { + namespace "com.expensify.reactnativebackgroundtask" + + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() + + } + + buildFeatures { + buildConfig true + } + + buildTypes { + release { + minifyEnabled false + } + } + + lintOptions { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + if (isNewArchitectureEnabled()) { + java.srcDirs += [ + "src/newarch", + // Codegen specs + "generated/java", + "generated/jni" + ] + } else { + java.srcDirs += ["src/oldarch"] + } + } + } +} + +repositories { + mavenCentral() + google() +} + +def kotlin_version = getExtOrDefault("kotlinVersion") + +dependencies { + //noinspection GradleDynamicVersion + implementation "com.facebook.react:react-android" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" +} + +if (isNewArchitectureEnabled()) { + react { + jsRootDir = file("../src/") + libraryName = "ReactNativeBackgroundTask" + codegenJavaPackageName = "com.expensify.reactnativebackgroundtask" + } +} diff --git a/modules/background-task/android/gradle.properties b/modules/background-task/android/gradle.properties new file mode 100644 index 000000000000..4eec12b62c7e --- /dev/null +++ b/modules/background-task/android/gradle.properties @@ -0,0 +1,5 @@ +ReactNativeBackgroundTask_kotlinVersion=1.9.24 +ReactNativeBackgroundTask_minSdkVersion=23 +ReactNativeBackgroundTask_targetSdkVersion=34 +ReactNativeBackgroundTask_compileSdkVersion=34 +ReactNativeBackgroundTask_ndkversion=26.1.10909125 diff --git a/modules/background-task/android/src/main/AndroidManifest.xml b/modules/background-task/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..cf2838eb09ee --- /dev/null +++ b/modules/background-task/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/BackgroundJobService.kt b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/BackgroundJobService.kt new file mode 100644 index 000000000000..4184727677a0 --- /dev/null +++ b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/BackgroundJobService.kt @@ -0,0 +1,25 @@ +package com.expensify.reactnativebackgroundtask + +import android.app.job.JobParameters +import android.app.job.JobService +import android.content.Intent +import android.util.Log +import com.facebook.react.ReactApplication + +class BackgroundJobService : JobService() { + override fun onStartJob(params: JobParameters?): Boolean { + val taskName = params?.extras?.getString("taskName") + val intent = Intent("com.expensify.reactnativebackgroundtask.TASK_ACTION").apply { + putExtra("taskName", taskName) + } + sendBroadcast(intent) + + // Job is done, return false if no more work is needed + return false + } + + override fun onStopJob(params: JobParameters?): Boolean { + // Return true to reschedule the job + return true + } +} diff --git a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt new file mode 100644 index 000000000000..395f822645b3 --- /dev/null +++ b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt @@ -0,0 +1,69 @@ +package com.expensify.reactnativebackgroundtask + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.Callback +import android.app.job.JobScheduler +import android.app.job.JobInfo +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.PersistableBundle +import android.util.Log + +class ReactNativeBackgroundTaskModule internal constructor(context: ReactApplicationContext) : + ReactNativeBackgroundTaskSpec(context) { + + private val taskReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val taskName = intent?.getStringExtra("taskName") + Log.d("ReactNativeBackgroundTaskModule", "Received task: $taskName") + emitOnBackgroundTaskExecution(taskName) + } + } + + init { + val filter = IntentFilter("com.expensify.reactnativebackgroundtask.TASK_ACTION") + reactApplicationContext.registerReceiver(taskReceiver, filter) + } + + override fun getName(): String { + return NAME + } + + @ReactMethod + override fun defineTask(taskName: String, taskExecutor: Callback, promise: Promise) { + try { + val jobScheduler = reactApplicationContext.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler + val componentName = ComponentName(reactApplicationContext, BackgroundJobService::class.java) + + val extras = PersistableBundle().apply { + putString("taskName", taskName) + } + + val jobInfo = JobInfo.Builder(taskName.hashCode() and 0xFFFFFF, componentName) + .setPeriodic(15 * 60 * 1000L) // 15 minutes in milliseconds + .setPersisted(true) // Job persists after reboot + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + .setExtras(extras) + .build() + + val resultCode = jobScheduler.schedule(jobInfo) + if (resultCode == JobScheduler.RESULT_SUCCESS) { + + promise.resolve(null); + } else { + promise.reject("ERROR", "Failed to schedule job") + } + } catch (e: Exception) { + promise.reject("ERROR", e.message) + } + } + + companion object { + const val NAME = "ReactNativeBackgroundTask" + } +} diff --git a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskPackage.kt b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskPackage.kt new file mode 100644 index 000000000000..dd0547f7ef35 --- /dev/null +++ b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskPackage.kt @@ -0,0 +1,35 @@ +package com.expensify.reactnativebackgroundtask + +import com.facebook.react.TurboReactPackage +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.NativeModule +import com.facebook.react.module.model.ReactModuleInfoProvider +import com.facebook.react.module.model.ReactModuleInfo +import java.util.HashMap + +class ReactNativeBackgroundTaskPackage : TurboReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return if (name == ReactNativeBackgroundTaskModule.NAME) { + ReactNativeBackgroundTaskModule(reactContext) + } else { + null + } + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { + val moduleInfos: MutableMap = HashMap() + val isTurboModule: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + moduleInfos[ReactNativeBackgroundTaskModule.NAME] = ReactModuleInfo( + ReactNativeBackgroundTaskModule.NAME, + ReactNativeBackgroundTaskModule.NAME, + false, // canOverrideExistingModule + false, // needsEagerInit + true, // hasConstants + false, // isCxxModule + isTurboModule // isTurboModule + ) + moduleInfos + } + } +} diff --git a/modules/background-task/android/src/newarch/ReactNativeBackgroundTaskSpec.kt b/modules/background-task/android/src/newarch/ReactNativeBackgroundTaskSpec.kt new file mode 100644 index 000000000000..2ee39c49c66d --- /dev/null +++ b/modules/background-task/android/src/newarch/ReactNativeBackgroundTaskSpec.kt @@ -0,0 +1,7 @@ +package com.expensify.reactnativebackgroundtask + +import com.facebook.react.bridge.ReactApplicationContext + +abstract class ReactNativeBackgroundTaskSpec internal constructor(context: ReactApplicationContext) : + NativeReactNativeBackgroundTaskSpec(context) { +} diff --git a/modules/background-task/android/src/oldarch/ReactNativeBackgroundTaskSpec.kt b/modules/background-task/android/src/oldarch/ReactNativeBackgroundTaskSpec.kt new file mode 100644 index 000000000000..138a1cc1d4af --- /dev/null +++ b/modules/background-task/android/src/oldarch/ReactNativeBackgroundTaskSpec.kt @@ -0,0 +1,11 @@ +package com.expensify.reactnativebackgroundtask + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.Promise + +abstract class ReactNativeBackgroundTaskSpec internal constructor(context: ReactApplicationContext) : + ReactContextBaseJavaModule(context) { + + abstract fun defineTask(taskName: String, taskExecutor: Callback, promise: Promise) +} diff --git a/modules/background-task/react-native.config.js b/modules/background-task/react-native.config.js index d532440e69b0..5bad2c0ec408 100644 --- a/modules/background-task/react-native.config.js +++ b/modules/background-task/react-native.config.js @@ -4,7 +4,9 @@ module.exports = { dependency: { platforms: { - android: null, + android: { + cmakeListsPath: 'build/generated/source/codegen/jni/CMakeLists.txt', + }, }, }, }; diff --git a/src/setup/backgroundTask/index.ios.ts b/src/setup/backgroundTask/index.native.ts similarity index 100% rename from src/setup/backgroundTask/index.ios.ts rename to src/setup/backgroundTask/index.native.ts diff --git a/src/setup/backgroundTask/index.ts b/src/setup/backgroundTask/index.ts index e69de29bb2d1..bb307a04703f 100644 --- a/src/setup/backgroundTask/index.ts +++ b/src/setup/backgroundTask/index.ts @@ -0,0 +1,2 @@ +// This file is intentionally empty as Background Tasks are currently only implemented +// for native mobile platforms. See `index.native.ts` for the native implementation.