@supersami/rn-foreground-service
Version:
A Foreground Service for React Native
433 lines (392 loc) • 12 kB
JavaScript
import {
NativeModules,
AppRegistry,
DeviceEventEmitter,
NativeEventEmitter,
Alert
} from 'react-native';
// ANDROID ONLY
// Copied and adapted from https://github.com/voximplant/react-native-foreground-service
// and https://github.com/zo0r/react-native-push-notification/
const ForegroundServiceModule = NativeModules.ForegroundService;
/**
* @property {number} id - Unique notification id
* @property {string} title - Notification title
* @property {string} message - Notification message
* @property {string} ServiceType - Foreground service types are Mandatory in Android 14
* @property {string} number - int specified as string > 0, for devices that support it, this might be used to set the badge counter
* @property {string} icon - Small icon name | ic_notification
* @property {string} largeIcon - Large icon name | ic_launcher
* @property {string} visibility - private | public | secret
* @property {boolean} ongoing - true/false if the notification is ongoing. The notification the service was started with will always be ongoing
* @property {number} [importance] - Importance (and priority for older devices) of this notification. This might affect notification sound One of:
* none - IMPORTANCE_NONE (by default),
* min - IMPORTANCE_MIN,
* low - IMPORTANCE_LOW,
* default - IMPORTANCE_DEFAULT
* high - IMPORTANCE_HIGH,
* max - IMPORTANCE_MAX
*/
const NotificationConfig = {};
/**
* @property {string} taskName - name of the js task configured with registerForegroundTask
* @property {number} delay - start task in delay miliseconds, use 0 to start immediately
* ... any other values passed to the task as well
*/
const TaskConfig = {};
class ForegroundService {
/**
* Registers a piece of JS code to be ran on the service
* NOTE: This must be called before anything else, or the service will fail.
* NOTE2: Registration must also happen at module level (not at mount)
* task will receive all parameters from runTask
* @param {task} async function to be called
*/
static registerForegroundTask(taskName, task) {
AppRegistry.registerHeadlessTask(taskName, () => task);
}
/**
* Start foreground service
* Multiple calls won't start multiple instances of the service, but will increase its internal counter
* so calls to stop won't stop until it reaches 0.
* Note: notificationConfig can't be re-used (becomes immutable)
* @param {NotificationConfig} notificationConfig - Notification config
* @return Promise
*/
static async startService(notificationConfig) {
console.log('Start Service Triggered');
return await ForegroundServiceModule.startService(notificationConfig);
}
/**
* Updates a notification of a running service. Make sure to use the same ID
* or it will trigger a separate notification.
* Note: this method might fail if called right after starting the service
* since the service might not be yet ready.
* If service is not running, it will be started automatically like calling startService.
* @param {NotificationConfig} notificationConfig - Notification config
* @return Promise
*/
static async updateNotification(notificationConfig) {
console.log(' Update Service Triggered');
return await ForegroundServiceModule.updateNotification(notificationConfig);
}
/**
* Cancels/dimisses a notification given its id. Useful if the service used
* more than one notification
* @param {number} id - Notification id to cancel
* @return Promise
*/
static async cancelNotification(id) {
console.log('Cancel Service Triggered');
return await ForegroundServiceModule.cancelNotification({id: id});
}
/**
* Stop foreground service. Note: Pending tasks might still complete.
* If startService will called multiple times, this needs to be called as many times.
* @return Promise
*/
static async stopService() {
console.log('Stop Service Triggered');
return await ForegroundServiceModule.stopService();
}
/**
* Stop foreground service. Note: Pending tasks might still complete.
* This will stop the service regardless of how many times start was called
* @return Promise
*/
static async stopServiceAll() {
return await ForegroundServiceModule.stopServiceAll();
}
/**
* Runs a previously configured headless task.
* Task must be able to self stop if the service is stopped, since it can't be force killed once started.
* Note: This method might silently fail if the service is not running, but will run successfully
* if the service is still spinning up.
* If the service is not running because it was killed, it will be attempted to be started again
* using the last notification available.
* @param {TaskConfig} taskConfig - Notification config
* @return Promise
*/
static async runTask(taskConfig) {
return await ForegroundServiceModule.runTask(taskConfig);
}
/**
* Returns an integer indicating if the service is running or not.
* The integer represents the internal counter of how many startService
* calls were done without calling stopService
* @return Promise
*/
static async isRunning() {
return await ForegroundServiceModule.isRunning();
}
}
const randHashString = len => {
return 'x'.repeat(len).replace(/[xy]/g, c => {
let r = (Math.random() * 16) | 0,
v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
};
//initial state
let tasks = {};
const samplingInterval = 500; //ms
let serviceRunning = false;
const deleteTask = taskId => {
delete tasks[taskId];
};
const taskRunner = async () => {
try {
if (!serviceRunning) return;
const now = Date.now();
let promises = [];
//iterate over all tasks
Object.entries(tasks).forEach(([taskId, task]) => {
//check if this task's execution time has arrived
if (now >= task.nextExecutionTime) {
//push this task's promise for later execution
promises.push(
Promise.resolve(task.task()).then(task.onSuccess, task.onError),
);
//if this is a looped task then increment its nextExecutionTime by delay for the next interval
if (task.onLoop) task.nextExecutionTime = now + task.delay;
//else delete the one-off task
else deleteTask(taskId);
}
});
//execute all tasks promises in parallel
await Promise.all(promises);
} catch (error) {
console.log('Error in FgService taskRunner:', error);
}
};
const register = ({config: {alert, onServiceErrorCallBack}}) => {
if (!serviceRunning) {
setupServiceErrorListener({
alert,
onServiceFailToStart: onServiceErrorCallBack,
});
return ForegroundService.registerForegroundTask('myTaskName', taskRunner);
}
};
const start = async ({
id,
title = id,
message = 'Foreground Service Running...',
ServiceType,
vibration = false,
visibility = 'public',
icon = 'ic_notification',
largeIcon = 'ic_launcher',
importance = 'max',
number = '1',
button = false,
buttonText = '',
buttonOnPress = 'buttonOnPress',
button2 = false,
button2Text = '',
button2OnPress = 'button2OnPress',
mainOnPress = 'mainOnPress',
progress,
color,
setOnlyAlertOnce,
}) => {
try {
if (!serviceRunning) {
await ForegroundService.startService({
id,
title,
message,
ServiceType,
vibration,
visibility,
icon,
largeIcon,
importance,
number,
button,
buttonText,
buttonOnPress,
button2,
button2Text,
button2OnPress,
mainOnPress,
progressBar: !!progress,
progressBarMax: progress?.max,
progressBarCurr: progress?.curr,
color,
setOnlyAlertOnce,
});
serviceRunning = true;
await ForegroundService.runTask({
taskName: 'myTaskName',
delay: samplingInterval,
loopDelay: samplingInterval,
onLoop: true,
});
} else console.log('Foreground service is already running.');
} catch (error) {
throw error;
}
};
const update = async ({
id,
title = id,
message = 'Foreground Service Running...',
ServiceType,
vibration = false,
visibility = 'public',
largeIcon = 'ic_launcher',
icon = 'ic_launcher',
importance = 'max',
number = '0',
button = false,
buttonText = '',
buttonOnPress = 'buttonOnPress',
button2 = false,
button2Text = '',
button2OnPress = 'button2OnPress',
mainOnPress = 'mainOnPress',
progress,
color,
setOnlyAlertOnce,
}) => {
try {
await ForegroundService.updateNotification({
id,
title,
message,
ServiceType,
vibration,
visibility,
largeIcon,
icon,
importance,
number,
button,
buttonText,
buttonOnPress,
button2,
button2Text,
button2OnPress,
mainOnPress,
progressBar: !!progress,
progressBarMax: progress?.max,
progressBarCurr: progress?.curr,
setOnlyAlertOnce,
color,
});
if (!serviceRunning) {
serviceRunning = true;
await ForegroundService.runTask({
taskName: 'myTaskName',
delay: samplingInterval,
loopDelay: samplingInterval,
onLoop: true,
});
}
} catch (error) {
throw error;
}
};
const stop = () => {
serviceRunning = false;
return ForegroundService.stopService();
};
const stopAll = () => {
serviceRunning = false;
return ForegroundService.stopServiceAll();
};
const is_running = () => serviceRunning;
const add_task = (
task,
{
delay = 5000,
onLoop = true,
taskId = randHashString(12),
onSuccess = () => {},
onError = () => {},
},
) => {
const _type = typeof task;
if (_type !== 'function')
throw `invalid task of type ${_type}, expected a function or a Promise`;
if (!tasks[taskId])
tasks[taskId] = {
task,
nextExecutionTime: Date.now(),
delay: Math.ceil(delay / samplingInterval) * samplingInterval,
onLoop: onLoop,
taskId,
onSuccess,
onError,
};
return taskId;
};
const update_task = (
task,
{
delay = 5000,
onLoop = true,
taskId = randHashString(12),
onSuccess = () => {},
onError = () => {},
},
) => {
const _type = typeof task;
if (_type !== 'function')
throw `invalid task of type ${_type}, expected a function or a Promise`;
tasks[taskId] = {
task,
nextExecutionTime: Date.now(),
delay: Math.ceil(delay / samplingInterval) * samplingInterval,
onLoop: onLoop,
taskId,
onSuccess,
onError,
};
return taskId;
};
const remove_task = taskId => deleteTask(taskId);
const is_task_running = taskId => (tasks[taskId] ? true : false);
const remove_all_tasks = () => (tasks = {});
const get_task = taskId => tasks[taskId];
const get_all_tasks = () => tasks;
const eventListener = callBack => {
let subscription = DeviceEventEmitter.addListener(
'notificationClickHandle',
callBack,
);
return function cleanup() {
subscription.remove();
};
};
const eventEmitter = new NativeEventEmitter(ForegroundServiceModule);
export function setupServiceErrorListener({onServiceFailToStart, alert}) {
const listener = eventEmitter.addListener('onServiceError', message => {
alert && Alert.alert('Service Error', message);
if (onServiceFailToStart) {
onServiceFailToStart();
}
stop();
});
return () => {
listener.remove();
};
}
const ReactNativeForegroundService = {
register,
start,
update,
stop,
stopAll,
is_running,
add_task,
update_task,
remove_task,
is_task_running,
remove_all_tasks,
get_task,
get_all_tasks,
eventListener,
};
export default ReactNativeForegroundService;