Converted Native to Turbo Module

This commit is contained in:
Tanmaya Biswal 2025-04-24 11:53:45 +05:30
parent b1f1f1e6ea
commit 9ae4a15260
15 changed files with 4571 additions and 368 deletions

299
App.tsx
View File

@ -8,29 +8,28 @@
* @format
*/
import React from 'react';
import React, {useEffect} from 'react';
import {
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
View,
Switch,
Button,
Alert,
PermissionsAndroid,
// ToastAndroid,
NativeModules,
// NativeModules,
} from 'react-native';
import BackgroundFetch from 'react-native-background-fetch';
import Geolocation, {
GeolocationError,
GeolocationResponse,
// GeolocationResponse,
} from '@react-native-community/geolocation';
const {ForegroundHeadlessModule} = NativeModules;
import NativeTaskRunner from './specs/NativeTaskRunner';
// const {ForegroundHeadlessModule} = NativeModules;
const Colors = {
gold: '#fedd1e',
@ -42,14 +41,37 @@ const Colors = {
};
/// Util class for handling fetch-event peristence in AsyncStorage.
import Event from './src/Event';
// import Event from './src/Event';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Location from 'expo-location';
Geolocation.setRNConfiguration({
skipPermissionRequests: true,
locationProvider: 'auto',
});
/*
function geoLocationPromise(): Promise<GeolocationResponse | GeolocationError> {
return new Promise((resolve, reject) => {
Geolocation.getCurrentPosition(
info => {
console.log(info);
resolve(info);
},
error => {
console.log(error);
reject(error);
},
{
timeout: 20000,
maximumAge: 0,
// enableHighAccuracy: true,
},
);
});
}
*/
const requestPermissions = async () => {
try {
const granted = await PermissionsAndroid.request(
@ -85,219 +107,32 @@ const requestPermissions = async () => {
}
};
function geoLocationPromise(): Promise<GeolocationResponse | GeolocationError> {
return new Promise((resolve, reject) => {
Geolocation.getCurrentPosition(
info => {
console.log(info);
resolve(info);
},
error => {
console.log(error);
reject(error);
},
{
timeout: 20000,
maximumAge: 0,
// enableHighAccuracy: true,
},
);
});
}
const App = () => {
const [enabled, setEnabled] = React.useState(false);
const [status, setStatus] = React.useState(-1);
const [events, setEvents] = React.useState<Event[]>([]);
React.useEffect(() => {
async function asyncTask() {
await requestPermissions();
initBackgroundFetch();
loadEvents();
useEffect(() => {
async function getCurrentLocation() {
let location = await Location.getLastKnownPositionAsync({});
console.log(location);
}
asyncTask();
}, []);
/// Configure BackgroundFetch.
///
const initBackgroundFetch = async () => {
const status: number = await BackgroundFetch.configure(
{
minimumFetchInterval: 15, // <-- minutes (15 is minimum allowed)
stopOnTerminate: false,
enableHeadless: true,
startOnBoot: false,
// Android options
forceAlarmManager: false, // <-- Set true to bypass JobScheduler.
requiredNetworkType: BackgroundFetch.NETWORK_TYPE_NONE, // Default
requiresCharging: false, // Default
requiresDeviceIdle: false, // Default
requiresBatteryNotLow: false, // Default
requiresStorageNotLow: false, // Default
},
async (taskId: string) => {
console.log('[BackgroundFetch] taskId', taskId);
if (taskId === 'react-native-background-fetch') {
// console.log('[BackgroundFetch] taskId', taskId);
const event = await Event.create(taskId, false);
// Update state.
setEvents(prev => [...prev, event]);
}
if (taskId === 'com.transistorsoft.customtask') {
// Geolocation.getCurrentPosition(
// async info => {
// console.log(info);
// const event = await Event.create(taskId, false, `lat-${info.coords.latitude};long-${info.coords.longitude}`);
// },
// err => console.log(err),
// {
// timeout: 10000,
// maximumAge: 10000,
// enableHighAccuracy: false,
// },
// );
async function xy() {
try {
const locInfo = (await geoLocationPromise()) as GeolocationResponse;
const event = await Event.create(
taskId,
false,
`lat-${locInfo.coords.latitude};long-${locInfo.coords.longitude}`,
);
// Update state.
setEvents(prev => [...prev, event]);
} catch (error) {
const g = error as GeolocationError;
if (g.message) {
const event = await Event.create(taskId, false, `${g.message}`);
// Update state.
setEvents(prev => [...prev, event]);
} else {
const event = await Event.create(taskId, false);
// Update state.
setEvents(prev => [...prev, event]);
}
}
// console.log('Running [customtask] here!');
}
// Finish.
BackgroundFetch.finish(taskId);
},
(taskId: string) => {
// Oh No! Our task took too long to complete and the OS has signalled
// that this task must be finished immediately.
console.log('[Fetch] TIMEOUT taskId:', taskId);
BackgroundFetch.finish(taskId);
},
);
setStatus(status);
await requestPermissions();
const x = await AsyncStorage.getItem('locationtask');
if (x === '1') {
setEnabled(true);
};
/// Load persisted events from AsyncStorage.
///
const loadEvents = () => {
Event.all()
.then(data => {
setEvents(data as Event[]);
})
.catch(error => {
Alert.alert('Error', 'Failed to load data from AsyncStorage: ' + error);
});
};
/// Toggle BackgroundFetch ON/OFF
///
const onClickToggleEnabled = (value: boolean) => {
setEnabled(value);
if (value) {
BackgroundFetch.start();
} else {
BackgroundFetch.stop();
setEnabled(false);
}
};
/// [Status] button handler.
///
const onClickStatus = () => {
BackgroundFetch.status().then((status: number) => {
let statusConst = '';
switch (status) {
case BackgroundFetch.STATUS_AVAILABLE:
statusConst = 'STATUS_AVAILABLE';
break;
case BackgroundFetch.STATUS_DENIED:
statusConst = 'STATUS_DENIED';
break;
case BackgroundFetch.STATUS_RESTRICTED:
statusConst = 'STATUS_RESTRICTED';
break;
getCurrentLocation();
} catch (error) {
console.log('Something went wrong', error);
}
Alert.alert('BackgroundFetch.status()', `${statusConst} (${status})`);
});
};
/// [scheduleTask] button handler.
/// Schedules a custom-task to fire in 5000ms
///
const onClickScheduleTask = () => {
const delay = 600000;
BackgroundFetch.scheduleTask({
taskId: 'com.transistorsoft.customtask',
stopOnTerminate: false,
enableHeadless: true,
delay: delay,
forceAlarmManager: false,
periodic: true,
})
.then(() => {
Alert.alert(
'scheduleTask',
'Scheduled task with delay: ' + delay + 'ms',
);
})
.catch(error => {
Alert.alert('scheduleTask ERROR', error);
});
};
/// Clear the Events list.
///
const onClickClear = () => {
Event.destroyAll();
setEvents([]);
};
/// Fetch events renderer.
///
const renderEvents = () => {
if (!events.length) {
return (
<Text style={{padding: 10, fontSize: 16}}>
Waiting for BackgroundFetch events...
</Text>
);
}
return events
.slice()
.reverse()
.map(event => (
<View key={event.key} style={styles.event}>
<View style={{flexDirection: 'row'}}>
<Text style={styles.taskId}>
{event.taskId}&nbsp;{event.isHeadless ? '[Headless]' : ''}
</Text>
</View>
<Text style={styles.remark}>Remark - {event.location}</Text>
<Text style={styles.timestamp}>{event.timestamp}</Text>
</View>
));
};
xy();
}, []);
async function onLocPress(): Promise<void> {
try {
@ -306,7 +141,18 @@ const App = () => {
// `lat-${locInfo.coords.latitude};long-${locInfo.coords.longitude}`,
// 5000,
// );
ForegroundHeadlessModule.startService();
if (enabled) {
return;
}
try {
// ForegroundHeadlessModule.startService();
NativeTaskRunner.startService();
setEnabled(true);
AsyncStorage.setItem('locationtask', '1');
} catch (error) {
console.log(error);
}
console.log('Did it run?');
} catch (error) {
const g = error as GeolocationError;
@ -319,19 +165,14 @@ const App = () => {
}
function onLocStop() {
ForegroundHeadlessModule.stopService();
AsyncStorage.getItem('watchId', (err, result) => {
if (err) {
console.log('Couldnt get WatchID', err);
return;
try {
// ForegroundHeadlessModule.stopService();
NativeTaskRunner.stopService();
setEnabled(false);
AsyncStorage.setItem('locationtask', '0');
} catch (error) {
console.log(error);
}
console.log(result);
if (result) {
Geolocation.clearWatch(+result);
}
});
}
return (
@ -342,19 +183,9 @@ const App = () => {
<Text style={styles.title}>BGFetch Demo</Text>
<Button title="Loc" onPress={onLocPress} />
<Button title="Stop" onPress={onLocStop} />
<Switch value={enabled} onValueChange={onClickToggleEnabled} />
</View>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={styles.eventList}>
{renderEvents()}
</ScrollView>
<View style={styles.toolbar}>
<Button title={'status: ' + status} onPress={onClickStatus} />
<Text>&nbsp;</Text>
<Button title="scheduleTask" onPress={onClickScheduleTask} />
<View style={{flex: 1}} />
<Button title="clear" onPress={onClickClear} />
<Switch value={enabled} />
</View>
</View>
</SafeAreaView>

View File

@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

View File

@ -45,7 +45,7 @@ public class ForegroundHeadlessService extends Service {
HeadlessJsTaskService.acquireWakeLockNow(context);
// Schedule next execution
handler.postDelayed(this, 60000);
handler.postDelayed(this, 120000);
}
};

View File

@ -1,4 +1,5 @@
package com.poclocationgather
import expo.modules.ReactActivityDelegateWrapper
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
@ -18,5 +19,5 @@ class MainActivity : ReactActivity() {
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled))
}

View File

@ -1,4 +1,7 @@
package com.poclocationgather
import android.content.res.Configuration
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
import android.app.Application
import com.facebook.react.PackageList
@ -11,16 +14,27 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import com.nativetaskrunner.NativeTaskRunnerPackage
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
add(ForegroundHeadlessPackage())
add(NativeTaskRunnerPackage())
}
// override fun getPackages(): List<ReactPackage> {
// val packages = PackageList(this).packages
// // Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
// packages.add(new ForegroundHeadlessPackage());
// packages.add(new NativeTaskRunnerPackage());
// return packages
// }
override fun getJSMainModuleName(): String = "index"
@ -28,10 +42,10 @@ class MainApplication : Application(), ReactApplication {
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
})
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
@ -40,5 +54,11 @@ class MainApplication : Application(), ReactApplication {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
}
}

View File

@ -29,10 +29,6 @@ allprojects {
// Android JSC is installed from npm
url("$rootDir/../node_modules/jsc-android/dist")
}
maven {
// react-native-background-fetch
url("${project(':react-native-background-fetch').projectDir}/libs")
}
}
}

View File

@ -1,6 +1,21 @@
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex ->
def command = [
'node',
'--no-warnings',
'--eval',
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
'react-native-config',
'--json',
'--platform',
'android'
].toList()
ex.autolinkLibrariesFromCommand(command)
}
rootProject.name = 'POCLocationGather'
include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle")
useExpoModules()

View File

@ -1,4 +1,14 @@
{
"name": "POCLocationGather",
"displayName": "POCLocationGather"
"displayName": "POCLocationGather",
"expo": {
"plugins": [
[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location."
}
]
]
}
}

View File

@ -16,7 +16,7 @@
/* Begin PBXContainerItemProxy section */
00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
containerPortal = 83CBB9F71A601CBA00E9B192;
proxyType = 1;
remoteGlobalIDString = 13B07F861A680F5B00A75B9A;
remoteInfo = POCLocationGather;
@ -85,7 +85,7 @@
name = Libraries;
sourceTree = "<group>";
};
83CBB9F61A601CBA00E9B192 = {
83CBB9F61A601CBA00E9B192 /* PBXGroup */ = {
isa = PBXGroup;
children = (
13B07FAE1A68108700A75B9A /* POCLocationGather */,
@ -143,7 +143,7 @@
/* End PBXNativeTarget section */
/* Begin PBXProject section */
83CBB9F71A601CBA00E9B192 /* Project object */ = {
83CBB9F71A601CBA00E9B192 = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1210;
@ -161,7 +161,7 @@
en,
Base,
);
mainGroup = 83CBB9F61A601CBA00E9B192;
mainGroup = 83CBB9F61A601CBA00E9B192 /* PBXGroup */;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
projectDirPath = "";
projectRoot = "";
@ -409,6 +409,7 @@
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
SDKROOT = iphoneos;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
@ -475,6 +476,7 @@
);
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
@ -501,5 +503,5 @@
};
/* End XCConfigurationList section */
};
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
rootObject = 83CBB9F71A601CBA00E9B192;
}

View File

@ -1,10 +1,12 @@
import UIKit
import ExpoModulesCore
import Expo
import React
import React_RCTAppDelegate
import ReactAppDependencyProvider
@main
class AppDelegate: RCTAppDelegate {
class AppDelegate: ExpoAppDelegate {
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
self.moduleName = "POCLocationGather"
self.dependencyProvider = RCTAppDependencyProvider()

View File

@ -1,3 +1,4 @@
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
# Resolve react_native_pods.rb with node to allow for hoisting
require Pod::Executable.execute_command('node', ['-p',
'require.resolve(
@ -15,7 +16,24 @@ if linkage != nil
end
target 'POCLocationGather' do
config = use_native_modules!
use_expo_modules!
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
else
config_command = [
'node',
'--no-warnings',
'--eval',
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
'react-native-config',
'--json',
'--platform',
'ios'
]
end
config = use_native_modules!(config_command)
use_react_native!(
:path => config[:reactNativePath],

4461
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,12 +9,21 @@
"start": "react-native start",
"test": "jest"
},
"codegenConfig": {
"name": "NativeTaskRunnerSpec",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.nativetaskrunner"
}
},
"dependencies": {
"@react-native-async-storage/async-storage": "^2.1.2",
"@react-native-community/geolocation": "^3.4.0",
"expo": "~52.0.0",
"expo-location": "~18.0.10",
"react": "18.3.1",
"react-native": "0.77.0",
"react-native-background-fetch": "^4.2.7"
"react-native": "0.77.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",

View File

@ -0,0 +1,9 @@
import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';
export interface Spec extends TurboModule {
startService(): void;
stopService(): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('NativeTaskRunner');

View File

@ -1,29 +1,33 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import Geolocation from '@react-native-community/geolocation';
// import AsyncStorage from '@react-native-async-storage/async-storage';
// import Geolocation from '@react-native-community/geolocation';
import * as Location from 'expo-location';
module.exports = async () => {
console.log('Background Geolocation task has hopefully started');
Geolocation.setRNConfiguration({
skipPermissionRequests: false,
locationProvider: 'playServices',
});
// Geolocation.setRNConfiguration({
// skipPermissionRequests: false,
// locationProvider: 'playServices',
// });
const watchId = Geolocation.getCurrentPosition(
pos =>
console.log(
'[Background location]',
pos.coords.latitude,
'//',
pos.coords.longitude,
),
err => console.log('Location Error while running in Background', err),
{
interval: 180000,
maximumAge: 0,
timeout: 20000,
},
);
// const watchId = Geolocation.getCurrentPosition(
// pos =>
// console.log(
// '[Background location]',
// pos.coords.latitude,
// '//',
// pos.coords.longitude,
// ),
// err => console.log('Location Error while running in Background', err),
// {
// interval: 180000,
// maximumAge: 0,
// timeout: 20000,
// },
// );
await AsyncStorage.setItem('watchId', `${watchId}`);
let location = await Location.getLastKnownPositionAsync({});
console.log(location);
// await AsyncStorage.setItem('watchId', `${watchId}`);
};