Skip to content

Commit

Permalink
Merge pull request Expensify#54616 from callstack-internal/szymonrybc…
Browse files Browse the repository at this point in the history
…zak/feat/android/setup-background-task

 feat(Android): setup background task
  • Loading branch information
luacmartins authored Jan 17, 2025
2 parents e00a8ef + 7472c60 commit efabde3
Show file tree
Hide file tree
Showing 12 changed files with 269 additions and 1 deletion.
5 changes: 5 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
android:theme="@style/AppTheme"
tools:replace="android:supportsRtl">

<service
android:name="com.expensify.reactnativebackgroundtask.BackgroundJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false" />

<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
Expand Down
104 changes: 104 additions & 0 deletions modules/background-task/android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
buildscript {
// Buildscript is evaluated before everything else so we can't use getExtOrDefault
ext.getExtOrDefault = {name ->
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"
}
}
5 changes: 5 additions & 0 deletions modules/background-task/android/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ReactNativeBackgroundTask_kotlinVersion=1.9.24
ReactNativeBackgroundTask_minSdkVersion=23
ReactNativeBackgroundTask_targetSdkVersion=34
ReactNativeBackgroundTask_compileSdkVersion=34
ReactNativeBackgroundTask_ndkversion=26.1.10909125
3 changes: 3 additions & 0 deletions modules/background-task/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
</manifest>
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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<String, ReactModuleInfo> = 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
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.expensify.reactnativebackgroundtask

import com.facebook.react.bridge.ReactApplicationContext

abstract class ReactNativeBackgroundTaskSpec internal constructor(context: ReactApplicationContext) :
NativeReactNativeBackgroundTaskSpec(context) {
}
Original file line number Diff line number Diff line change
@@ -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)
}
4 changes: 3 additions & 1 deletion modules/background-task/react-native.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
module.exports = {
dependency: {
platforms: {
android: null,
android: {
cmakeListsPath: 'build/generated/source/codegen/jni/CMakeLists.txt',
},
},
},
};
File renamed without changes.
2 changes: 2 additions & 0 deletions src/setup/backgroundTask/index.ts
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit efabde3

Please sign in to comment.