Issue
- Goal: I want to find out if a device that has my React Native app installed is in use or has been in use looking a certain timeframe in the past. If an allowed inactivity timeframe is exceeded, I want to take an action.
- Constraint: This detection of user activity should happen without the user having to interact with the app itself. Sleep detection apps already do this by using sensors apparently but accessing sensors while in background doesn't work for me in React Native (see table below).
- Related question: Is there a working method in purely native (Java/Kotlin, Swift/Objective-C) if I would not use React Native?
Following the methods I evaluated but was not able to achieve a working and reliable result with.
Method | Problem |
---|---|
AppState | would require the user to actively use the app |
battery drain | unreliable because of varying battery life between devices |
screen lock | did not find a way to get this information |
screen on time | did not find a way to get this information |
motion (magnetometer) | did not find a way to read sensors while the app is in background using expo-sensors , react-native-sensors with react-native-background-fetch |
(geo)location | would not detect use while the device is still |
app open event | the assumption is to get different results based on the app/device state but Linking.openURL() for a supported URL resolves in every state |
permission request | the assumption is to get different results based on the app/device state but PermissionAndroid.request() doesn't execute in background using react-native-background-fetch |
Another thing to have in mind: Background tasks can only be executed every ~15 minutes. Indeed, there exist foreground services but they would be the second choice.
Solution
After debating many options, I went with the following concept:
- The user is interacting with the device while it lays still. In this case, I interpret the user being active when the device either unlocks which can be detected setting up BroadcastReceiver for the ACTION_USER_PRESENT intent. I expect the timeframe to be relatively short before unlocking again or displacing the device. I also considered screen on/off but had to realize this provokes false positives like incoming phone calls and didn't want to bother filtering them out.
- The user is displacing the device while (not) interacting with it. For this case, I've set up SensorEventListener for rotation vector sensor to determine rotational movement and linear accelerometer sensor to determine straight movement without rotation. Despite they're likely to intermingle, considering both makes the detection even more accurate. Rotational movement involves comparison of previous and current values while only the current value of acceleration is of interest. For both, I compare the absolute sum of values against a threshold. Currently, the acceleration threshold is 0.5 and rotational movement is 0.05. I found the thresholds by experimenting with my test device while observing value logs, though it would be sensible to monitor and analyze some plots to find the most effective thresholds. For example, thresholds mustn't be too low as otherwise vibration may cause exceedances. Still, I'm unsure about the cross-device validity of my so ascertained thresholds. Calibration may be necessary.
As opposed to intents which can be handled by background services, constantly listening to sensors means performing a long-running task. By the android standard, such tasks need to be executed as ForegroundService. It's important that service has continuous CPU time to perform the sensor readings. To prevent deep sleep, there is the concept of WakeLock, however I haven't experienced any problems with a bare foreground service so far although it does not imply a wake lock.
To implement and integrate this with React Native, I went with custom native code. The foreground service sets up intent receiver and sensor listeners. It is either started or stopped from the React Native app using an exported native method or started automatically at device boot leveraging the ACTION_BOOT_COMPLETED. Finally, I'd love to move more code to the React Native side and there is a package react-native-foreground-service but I currently don't see a reliable way to receive intents in React Native and start the React Native app respectively the foreground service on boot. Sending events from native to JavaScript requires the app to be running so that the bridge is available. In view of prospective iOS implementation on the other hand, it's practical to have the native bits encapsulated in the native projects.
Answered By - alexanderdavide
Answer Checked By - Willingham (JavaFixing Volunteer)