Skip to content

Commit

Permalink
Merge pull request zingolabs#603 from juanky201271/dev_android_backgr…
Browse files Browse the repository at this point in the history
…ound_task_sync

New approach for Android & IOS Sync Background
  • Loading branch information
zancas authored Feb 29, 2024
2 parents f58ee7e + c8b82d5 commit b4b9bbe
Show file tree
Hide file tree
Showing 24 changed files with 626 additions and 407 deletions.
5 changes: 3 additions & 2 deletions __tests__/LoadedApp.snapshot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ThemeType } from '../app/types';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { I18n } from 'i18n-js';
import { StackScreenProps } from '@react-navigation/stack';
import { ServerType } from '../app/AppState';
import { BackgroundType, ServerType } from '../app/AppState';

// Crea un mock para el constructor de I18n
jest.mock('i18n-js', () => ({
Expand Down Expand Up @@ -121,10 +121,11 @@ describe('Component LoadedApp - test', () => {
const sendAll = false;
const privacy = false;
const mode = 'basic';
const background = {
const background: BackgroundType = {
batches: 0,
message: '',
date: 0,
dateEnd: 0,
};
const readOnly = false;
const receive = render(
Expand Down
5 changes: 3 additions & 2 deletions __tests__/LoadingApp.snapshot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ThemeType } from '../app/types';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { I18n } from 'i18n-js';
import { StackScreenProps } from '@react-navigation/stack';
import { ServerType } from '../app/AppState';
import { BackgroundType, ServerType } from '../app/AppState';

// Crea un mock para el constructor de I18n
jest.mock('i18n-js', () => ({
Expand Down Expand Up @@ -121,10 +121,11 @@ describe('Component LoadingApp - test', () => {
const sendAll = false;
const privacy = false;
const mode = 'basic';
const background = {
const background: BackgroundType = {
batches: 0,
message: '',
date: 0,
dateEnd: 0,
};
const firstLaunchingMessage = false;
const receive = render(
Expand Down
5 changes: 3 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,9 @@ android {
//applicationId 'com.ZingoMobile' // @Test
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 131 // Real
versionCode 145 // Real
//versionCode 117 // @Test
versionName "zingo-1.3.3" // Real
versionName "zingo-1.3.4" // Real
missingDimensionStrategy 'react-native-camera', 'general'
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
Expand Down Expand Up @@ -297,6 +297,7 @@ dependencies {
implementation jscFlavor
}
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.5.0"

def work_version = "2.7.1"

Expand Down
1 change: 0 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<service android:name=".BackgroundSync" />
</application>

<queries>
Expand Down
21 changes: 0 additions & 21 deletions android/app/src/main/java/org/ZingoLabs/Zingo/BackgroundSync.kt

This file was deleted.

254 changes: 254 additions & 0 deletions android/app/src/main/java/org/ZingoLabs/Zingo/BackgroundSyncWorker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
package org.ZingoLabs.Zingo

import android.content.Context
import android.os.Build
import androidx.work.Worker
import androidx.work.WorkerParameters
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import java.io.File
import java.util.*
import org.json.JSONObject
import java.nio.charset.StandardCharsets
import com.facebook.react.bridge.ReactApplicationContext
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atTime
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.until
import kotlin.random.Random
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import kotlin.time.toJavaDuration

class BackgroundSyncWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {

@RequiresApi(Build.VERSION_CODES.O)
override fun doWork(): Result {
val reactContext = ReactApplicationContext(MainApplication.getAppContext())
val rpcModule = RPCModule(reactContext)

Log.i("SCHEDULED_TASK_RUN", "Task running")

// save the background JSON file
val timeStampStart = Date().time / 1000
val timeStampStrStart = timeStampStart.toString()
val jsonBackgroundStart = "{\"batches\": \"0\", \"message\": \"Starting OK.\", \"date\": \"$timeStampStrStart\", \"dateEnd\": \"0\"}"
rpcModule.saveBackgroundFile(jsonBackgroundStart)
Log.i("SCHEDULED_TASK_RUN", "background json file SAVED $jsonBackgroundStart")

// checking if the wallet file exists
val exists: Boolean = walletExists()

if (exists) {
RustFFI.initlogging()

// check the Server, because the task can run without the App.
val balance = RustFFI.execute("balance", "")
Log.i("SCHEDULED_TASK_RUN", "Testing if server is active: $balance")
if (balance.lowercase().startsWith("error")) {
// this means this task is running with the App closed
loadWalletFile(rpcModule)
} else {
// this means the App is open,
// stop syncing first, just in case.
stopSyncingProcess()
}

// interrupt sync to false, just in case it is true.
val noInterrupting = RustFFI.execute("interrupt_sync_after_batch", "false")
Log.i("SCHEDULED_TASK_RUN", "Not interrupting sync: $noInterrupting")

// the task is running here blocking this execution until this process finished:
// 1. finished the syncing.

Log.i("SCHEDULED_TASK_RUN", "sync BEGIN")
val syncing = RustFFI.execute("sync", "")
Log.i("SCHEDULED_TASK_RUN", "sync END: $syncing")

} else {
Log.i("SCHEDULED_TASK_RUN", "No exists wallet file END")
// save the background JSON file
val timeStampError = Date().time / 1000
val timeStampStrError = timeStampError.toString()
val jsonBackgroundError = "{\"batches\": \"0\", \"message\": \"No active wallet KO.\", \"date\": \"$timeStampStrStart\", \"dateEnd\": \"$timeStampStrError\"}"
rpcModule.saveBackgroundFile(jsonBackgroundError)
Log.i("SCHEDULED_TASK_RUN", "background json file SAVED $jsonBackgroundError")
return Result.failure()

}

// save the wallet file with the new data from the sync process
rpcModule.saveWallet()
Log.i("SCHEDULED_TASK_RUN", "wallet file SAVED")

// save the background JSON file
val timeStampEnd = Date().time / 1000
val timeStampStrEnd = timeStampEnd.toString()
val jsonBackgroundEnd = "{\"batches\": \"0\", \"message\": \"Finished OK.\", \"date\": \"$timeStampStrStart\", \"dateEnd\": \"$timeStampStrEnd\"}"
rpcModule.saveBackgroundFile(jsonBackgroundEnd)
Log.i("SCHEDULED_TASK_RUN", "background json file SAVED $jsonBackgroundEnd")

return Result.success()
}

private fun loadWalletFile(rpcModule: RPCModule) {
// I have to init from wallet file in order to do the sync
// and I need to read the settings.json to find the server & chain type
MainApplication.getAppContext()?.openFileInput("settings.json")?.use { file ->
val settingsBytes = file.readBytes()
file.close()
val settingsString = settingsBytes.toString(Charsets.UTF_8)
val jsonObject = JSONObject(settingsString)
val server = jsonObject.getJSONObject("server").getString("uri")
val chainhint = jsonObject.getJSONObject("server").getString("chain_name")
Log.i(
"SCHEDULED_TASK_RUN",
"Opening the wallet file - No App active - server: $server chain: $chainhint"
)
rpcModule.loadExistingWalletNative(server, chainhint)
}
}

private fun stopSyncingProcess() {
var status = RustFFI.execute("syncstatus", "")
Log.i("SCHEDULED_TASK_RUN", "status response $status")

var data: ByteArray = status.toByteArray(StandardCharsets.UTF_8)
var jsonResp = JSONObject(String(data, StandardCharsets.UTF_8))
var inProgressStr: String = jsonResp.optString("in_progress")
var inProgress: Boolean = inProgressStr.toBoolean()

Log.i("SCHEDULED_TASK_RUN", "in progress value $inProgress")

while (inProgress) {
// interrupt
val interrupting = RustFFI.execute("interrupt_sync_after_batch", "true")
Log.i("SCHEDULED_TASK_RUN", "Interrupting sync: $interrupting")

// blocking the thread for 0.5 seconds.
Thread.sleep(500)

status = RustFFI.execute("syncstatus", "")
Log.i("SCHEDULED_TASK_RUN", "status response $status")

data = status.toByteArray(StandardCharsets.UTF_8)
jsonResp = JSONObject(String(data, StandardCharsets.UTF_8))
inProgressStr = jsonResp.optString("in_progress")
inProgress = inProgressStr.toBoolean()

Log.i("SCHEDULED_TASK_RUN", "in progress value $inProgress")
}

Log.i("SCHEDULED_TASK_RUN", "sync process STOPPED")

}

private fun walletExists(): Boolean {
// Check if a wallet already exists
val file = File(MainApplication.getAppContext()?.filesDir, "wallet.dat")
return if (file.exists()) {
Log.i("SCHEDULED_TASK_RUN", "Wallet exists")
true
} else {
Log.i("SCHEDULED_TASK_RUN", "Wallet DOES NOT exist")
false
}
}
}

class BSCompanion {
companion object {
private const val taskID = "Zingo_Processing_Task_ID"
private val SYNC_PERIOD = 24.hours
private val SYNC_DAY_SHIFT = 1.days // Move to tomorrow
private val SYNC_START_TIME_HOURS = 3.hours // Start around 3 a.m. at night
private val SYNC_START_TIME_MINUTES = 60.minutes // Randomize with minutes until 4 a.m.
@RequiresApi(Build.VERSION_CODES.O)
fun scheduleBackgroundTask() {
val reactContext = ReactApplicationContext(MainApplication.getAppContext())

// zancas requeriment, not plug-in, reverted.
val constraints = Constraints.Builder()
.setRequiresStorageNotLow(false) // less restricted
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build()

// PRODUCTION - next day between 3:00 and 4:00 am.
val targetTimeDiff = calculateTargetTimeDifference()

Log.i("SCHEDULING_TASK", "calculated target time DIFF $targetTimeDiff")

val workRequest = PeriodicWorkRequest.Builder(BackgroundSyncWorker::class.java, SYNC_PERIOD.toJavaDuration())
.setConstraints(constraints)
.setInitialDelay(targetTimeDiff.toJavaDuration())
.build()

Log.i("SCHEDULING_TASK", "Enqueuing the background task - Background")
WorkManager.getInstance(reactContext)
.enqueueUniquePeriodicWork(
taskID,
ExistingPeriodicWorkPolicy.REPLACE,
workRequest
)

Log.i("SCHEDULING_TASK", "Task info ${WorkManager.getInstance(reactContext).getWorkInfosForUniqueWork(
taskID).get()}")
}

private fun calculateTargetTimeDifference(): Duration {
val currentTimeZone: TimeZone = TimeZone.currentSystemDefault()

val now: Instant = Clock.System.now()

val targetTime =
now
.plus(SYNC_DAY_SHIFT)
.toLocalDateTime(currentTimeZone)
.date
.atTime(
hour = SYNC_START_TIME_HOURS.inWholeHours.toInt(),
// Even though the WorkManager will trigger the work approximately at the set time, it's
// better to randomize time in 3-4 a.m. This generates a number between 0 (inclusive) and 60
// (exclusive)
minute = Random.nextInt(0, SYNC_START_TIME_MINUTES.inWholeMinutes.toInt())
)

val targetTimeTime = targetTime.time
val targetTimeDate = targetTime.date
Log.i("SCHEDULING_TASK", "calculated target time $targetTimeTime and date $targetTimeDate")

return now.until(
other = targetTime.toInstant(currentTimeZone),
unit = DateTimeUnit.MILLISECOND,
timeZone = currentTimeZone
).toDuration(DurationUnit.MILLISECONDS)
}

fun cancelExecutingTask() {
val reactContext = ReactApplicationContext(MainApplication.getAppContext())

// run interrupt sync, just in case.
val interrupting = RustFFI.execute("interrupt_sync_after_batch", "true")
Log.i("SCHEDULED_TASK_RUN", "Interrupting sync: $interrupting")

Log.i("SCHEDULING_TASK", "Cancel background Task")
WorkManager.getInstance(reactContext)
.cancelUniqueWork(taskID)
}

}
}
Loading

0 comments on commit b4b9bbe

Please sign in to comment.