This differential implements native methods to be exposed to JS to request notifications permissions.
Details
Build the app on Android 13 device. Call CommAndroidNotifications.requestNotificationsPermission() from
ensurePushNotifsEnabled in push-handler.js. Ensure that prompt to grant permissions is visible ONLY after log in and that decision
passed to the prompt govoerns whether application can display notifs or not.
Diff Detail
- Repository
- rCOMM Comm
- Lint
Lint Not Applicable - Unit
Tests Not Applicable
Event Timeline
Note to reviewers: this is addressing an Urgent issue, so would be great to prioritize!
Besides nits, some considerations about promise handling.
native/android/app/src/main/java/app/comm/android/MainActivity.java | ||
---|---|---|
84–86 ↗ | (On Diff #23924) | You could do the request code equality check before the for loop and exit early. |
native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java | ||
28 ↗ | (On Diff #23924) | Nit: I know it's Java but this name is too long and difficult to read ;) Also, the default value is null so it can be omitted (up to you) |
158 ↗ | (On Diff #23924) | If this function is accidentally called twice from JS before the first promise is resolved, we have a state where one of them is never resolved, because it'd be overwritten here. if (notificationsPermissionRequestResultPromise.get() != null) { promise.reject(new RuntimeException("another permission request is already pending"); } notificationsPermissionRequestResultPromise.set(promise); |
176–177 ↗ | (On Diff #23924) | Doesn't this throw NPE if the promise is somehow null? I know this shouldn't happen, but it's a good practice |
native/android/app/src/main/java/app/comm/android/MainActivity.java | ||
---|---|---|
84–86 ↗ | (On Diff #23924) | I understand, but I implemented it this way with purpose. Perhaps in future we might use requestPermission for another type of runtime permission. In this case it will be easy to modify this callback by adding new if statement. If I add early exit now, we might need to remove it in future. |
native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java | ||
158 ↗ | (On Diff #23924) | If it is a common practice to reject then I will stick to the conversion. I was thinking about this actually how to handle already ongoing request. I was inspired by solution adopted by Notifee library: https://github.com/invertase/notifee/pull/523/commits/273ff30f99c0ee17d30c62da18ed0cce93fd7731. After reading through their code I understand they substitute already existing promise with new one. On the other hand, they don't use any synchronization primitives - they just keep raw reference to the promise. It just doesn't sound right to me so I am not strongly inclined to consider their implementation as a gold standard. |
Use compareAndSet sicne it is safer and cleaner
👍 for this change
native/android/app/src/main/java/app/comm/android/MainActivity.java | ||
---|---|---|
84–86 ↗ | (On Diff #23924) | Ok, that's fair |
native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java | ||
---|---|---|
32 ↗ | (On Diff #23975) | Can you add a code comment above this line explaining what 11111 means, or linking to somewhere that does? |
116 ↗ | (On Diff #23975) | I think it's confusing that this method has the same name as the one on line 189 |
132 ↗ | (On Diff #23975) | Same concern about overloading the method name |
160 ↗ | (On Diff #23975) | Is it possible to get here through a programmer error on the JS side? |
195–202 ↗ | (On Diff #23975) | I don't like the if () { return true } else { return false } pattern... I think it is more simply laid out this way: return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU; |
native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java | ||
---|---|---|
32 ↗ | (On Diff #23975) | This code can be any number, it's used to ensure that onRequestPermissionsResult() is triggered as a response to our request. |
160 ↗ | (On Diff #23975) | Yes, that was discussed offline and this rejection addresses my comment above: https://phab.comm.dev/D7106?id=23924#inline-46661 This rejection doesn't have to be explicitly handled on JS side, it's just a kind of 'invariant' note that this was accidentally called twice. |
native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java | ||
---|---|---|
32 ↗ | (On Diff #23975) | Got it – would be good to explain this in a code comment |
160 ↗ | (On Diff #23975) | Ah, so you're saying it's possible for this line to be triggered as a result of JS programmer error? Can you clarify what sort of error the JS programmer would have to make? Eg. is there an expectation that the JS programmer will not call requestNotificationsPermission before the last requestNotificationsPermission is complete? |
native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java | ||
---|---|---|
32 ↗ | (On Diff #23975) | I will add a comment. And yes - @bartek explanation is exactly the reason this request code is hardcoded. Additionally I chose the number 11111 since the same number was used by Notifee library in their PR that migrates to Android 13: https://github.com/invertase/notifee/blob/main/android/src/main/java/app/notifee/core/Notifee.java#L49 |
116 ↗ | (On Diff #23975) | Why is it confusing? I want to use the same functionality both in this Java class and in JS via exposed ReactMethod. I wanted to avoid repeating code that calls NotificationManagerCompat api/check for SDK_INT. |
160 ↗ | (On Diff #23975) | I think this is exactly the case @bartek wanted to handle. We expect this method not to be called twice in JS before the first completes. If JS programmer make this error they will see an exception that will force them to fix it. However if it is not acceptable to enforce this on the JS side I can work on another approach to handle this case. |
195–202 ↗ | (On Diff #23975) | Sure - I will change it. |
native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java | ||
---|---|---|
116 ↗ | (On Diff #23975) | My understanding is that these are two different methods, just with the same method name. Java lets you do this because it allows method name overloading. I think it's confusing because these two methods are doing different things. I'm not sure what you mean when you say "I want to use the same functionality". I don't understand how renaming your method to be more clear affects your ability to use functionality, or causes code repetition. Can you clarify what code you think will need to be repeated if you rename the methods on line 189 and 194? |
160 ↗ | (On Diff #23975) | Are we really prepared to guarantee that on the JS side? I suspect that this API will be triggered by a React state transition (eg. in a React.useEffect or in componentDidUpdate), which means that the JS side has to cache the promise in order to guarantee that it will only be executed once. We can do this in JS, but there are two downsides in comparison to doing it in Java:
On the other hand, this synchronization might be harder to do in Java. I don't have context on why we opted not to do it in Java – can you please share if there are any reasons? |
native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java | ||
---|---|---|
160 ↗ | (On Diff #23975) | I don't understand why we're wasting so much review cycle time on such a minor thing. I'm starting to wonder if it'd be better to revert my suggestion just to avoid all that fuss. Seriously, it's just a good practice to reject subsequent promises if one is still in progress. That's all. This is a way to prevent accidentally opening two native permission/image-picker/whatever interfaces at once. I've never seen that anybody ever cared about this that much, moreover I haven't seen anybody explicitly handling this in JS. It's obvious that you want to display it once (= call it once at a time = don't call it again until awaited). So in a typical scenario, this will never throw. |
native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java | ||
---|---|---|
160 ↗ | (On Diff #23975) | It is less about synchronization but more about what should we do if we issue a second call to this method before promise from the first call completes. My first idea was to atomically substitute the first promise with the second one. However this would leave this the first promise unresolved. Therefore @bartek requested to change this behaviour by rejecting the second promise. Alternatively we can just resolve the second promise to the value returned by hasPermission method. Even if the second promise resolves to invalid value (since the interaction between user and notifications permission prompt is still ongoing and may change hasPermission value) the the first will resolve to the correct value and will fix the state. |
native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java | ||
---|---|---|
116 ↗ | (On Diff #23975) | I wanted not to repeat those fragments of code: if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { // Application has to explicitly request notifications permission from // user since Android 13. Older versions grant them by default. Details: // https://developer.android.com/develop/ui/views/notifications/notification-permission return true; } return false; and return NotificationManagerCompat.from(getReactApplicationContext()) .areNotificationsEnabled(); I needed them both in Java code and in JS-exported methods, so I decided to extract them to private Java methods. I used overloading to name those methods since it was natural for me. |
native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java | ||
---|---|---|
160 ↗ | (On Diff #23975) |
Exactly!
Order of resolving them is not guaranteed so we could have a race condition here, I'd just leave it as is |
native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java | ||
---|---|---|
160 ↗ | (On Diff #23975) | After gathering the discussion above my statement on this is that:
|
- Replace method overloading with different names.
- Simplify check for Android OS version.
- Add comment to explain hardcoded request code for notifications permission.
I'm starting to wonder if it'd be better to revert my suggestion just to avoid all that fuss.
Reverting your suggestion will not avoid any "fuss" at this point. The fuss is not about your suggestion (which is better than nothing), but rather about the requirement that this API be called one-at-a-time.
This comes across as trying to hide issues from a reviewer in order to avoid having to address feedback.
Seriously, it's just a good practice to reject subsequent promises if one is still in progress.
In my experience, if you have something async that can only be run one-at-a-time, the "good practice" is to return an existing promise ("dedup promises") rather than reject subsequent promises.
I've never seen that anybody ever cared about this that much
I would appreciate if you could engage with the discussion instead of making personal comments.
moreover I haven't seen anybody explicitly handling this in JS
That's interesting. Deduping promises is a common idiom in JS, and is basically the reason that Promise.resolve exists. Here's an example from our codebase.
It's obvious that you want to display it once (= call it once at a time = don't call it again until awaited). So in a typical scenario, this will never throw.
I am not clear on what a "typical scenario" is, but as mentioned above I suspect that this API will be triggered by a React state transition, eg. a React.useEffect. It is not at all crazy to imagine somebody adding a new parameter to the dep list of such an effect, causing the effect to trigger again whenever that new parameter changes.
In my experience, relying on a React state transition to only trigger "one at a time" is a recipe for disaster.
I don't understand why we're wasting so much review cycle time on such a minor thing.
I don't think it needs to be all that complicated, and I agree we are spending too much time on review here. Here are some easy solutions I would be open to:
- Leave this Java code as-is, but add code in JavaScript to dedup promises (assign promise to a variable, if it already exists return it, otherwise create a new one)
- Instead of using 11111 every time, assign a counter variable in Java, and increment it for every call. (I assume the reuse of 11111 is the concern here – let me know if I am misunderstanding)
- Replacing notificationsPermissionRequestPromise with a collection of promises. If the collection is non-empty when requestNotificationsPermission is called, then the promise will be added to the collection without calling ActivityCompat.requestPermissions
- Using a React.useRef on the JS side to guarantee that the effect only runs one-at-a-time
After gathering the discussion above my statement on this is that:
- I would follow @bartek approach since as he stated it is a common practice he encountered in open source projects.
- If there is no acceptance to this approach I will make this method resolve not to two but to three values: false, true, null. The promise resolved to null will be interpreted in JS code that the request is already in progress and another promise will shortly resolve to some meaningful value we can react to (by querying for device token or setting it to null). It will probably be an early return in JS. Alternatively to resolving to ?boolean we can resolve to string constants such as GRANTED, DENIED and REQUEST_ALREADY_IN_PROGRESS.
If we do want to take the approach of "handle it in JS" (I'm open to this), I think my preference is option 1 from my comment above:
- Leave this Java code as-is, but add code in JavaScript to dedup promises (assign promise to a variable, if it already exists return it, otherwise create a new one)
If @marcin is down for that, I think we can move forward with this diff (Java side) as-is, and follow-up on the JavaScript side in D7120. Otherwise if you want to take a Java-based approach, feel free to re-request review and in D7120.
First of all, I'm sorry that my previous message sounds personal to you, I didn't mean it in any case.
I'll try to clarify my point in other words:
- By a "typical scenario" of API called once-at-a-time I meant opening a modal interface (permission request, opening a picker, etc) - you don't want to open it again if it's already open or you'd end up with glitchy UX
- In JS, a promise should be always resolved or rejected, so even if two "open modal and wait for result" promises are pending, we still want only one modal = one of the promises should be rejected. From Java perspective, they're always two separate promises (they ''promise'' to always either resolve or reject). You cannot dedupe them here.
- That rejection message means "you're trying to open this permission modal again but it's already open. change your code to not do this" - this means that the JS call to this API should be revisited (e,g, react effect).
Taking the above considerations and possible suggestions, I'd go with the JS-only options as well.
1st one (dedupe promises in JS) is the most reasonable to me, especially when this API is called in an effect.
2 . (request code counter) doesn't seem possible without 3.
and 3 (queue in Java) is in my opinion an overkill for such a simple API.
I don't have an opinion on 4.
But I'll let @marcin decide on the best approach here.