UNPKG

@supersami/rn-foreground-service

Version:

A Foreground Service for React Native

397 lines (358 loc) 11.1 kB
import { NativeModules, AppRegistry, DeviceEventEmitter } 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} 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 = () => { if (!serviceRunning) return ForegroundService.registerForegroundTask("myTaskName", taskRunner); }; const start = async ({ id, title = id, message = "Foreground Service Running...", 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, }) => { try { if (!serviceRunning) { await ForegroundService.startService({ id, title, message, vibration, visibility, icon, largeIcon, importance, number, button, buttonText, buttonOnPress, button2, button2Text, button2OnPress, mainOnPress, progressBar: !!progress, progressBarMax: progress?.max, progressBarCurr: progress?.curr, color, }); 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...", 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, }) => { try { await ForegroundService.updateNotification({ id, title, message, vibration, visibility, largeIcon, icon, importance, number, button, buttonText, buttonOnPress, button2, button2Text, button2OnPress, mainOnPress, progressBar: !!progress, progressBarMax: progress?.max, progressBarCurr: progress?.curr, }); 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 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;