Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

android notif: Replace our NotificationIntentService with directly opening MainActivity #5146

Merged
merged 17 commits into from
Dec 8, 2021

Conversation

chrisbobbe
Copy link
Contributor

Fixes: #5145

@@ -11,6 +11,25 @@ import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
import com.zulipmobile.notifications.*
import com.zulipmobile.sharing.maybeHandleShareIntent

/* Returns true just if we did handle the intent. */
fun maybeHandleOpenNotificationIntent(intent: Intent): Boolean {
Copy link
Contributor Author

@chrisbobbe chrisbobbe Dec 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we probably want this to live somewhere else (the share-to counterpart lives in SharingHelper.kt), and probably have a better name? I haven't settled on anything definite for either yet though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Looking at this function and now looking at the share-to version beside it, I think what I really want is to have those two bits of code next to each other -- and probably cleanest as a single when (intent.action), with sub-conditions as needed (like looking for EXTRA_NOTIFICATION_DATA, though actually the cleanest thing there -- which can be a followup -- is probably to parse the outermost parts of the URL.)

IOW rather than align the name of maybeHandleIntent to reflect where it lives, align where it lives to reflect its name: move it to be a private method on MainActivity, and then later in the branch add the open-notification case to it.

Part of why I'd like them to be next to each other is that they're carving out two different swaths of the same space of possibilities -- if we were to have any two things like this that could overlap in what they covered, then the order we called them in would start mattering, and it'd probably be a bug. So it's helpful to have them next to each other and ideally in the same when statement, so that it's easy to see they're mutually exclusive and keep them that way.

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Glad to have this getting cleaned up. Comments below.

when (appStatus) {
null, ReactAppStatus.NOT_RUNNING, ReactAppStatus.BACKGROUND ->
launchMainActivity(application as Context)
ReactAppStatus.FOREGROUND -> Unit
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any reasons given for the conditional; possibly it's
an unquestioned relic of the Wix library we used to use.

Indeed the behavior goes back to the Wix library. Looks like it's basically unchanged since 9eddca8:

android notif: Use standard upstream API for watching RN lifecycle.

This has exactly the same effect as the 80 lines of code in the wix
library's ReactAppLifecycleFacade.

with the logic a bit refactored in 9b0157d and 0feb940.

I don't think having the conditional is necessarily bad, given that launching isn't going to do anything useful when already in the foreground. But with the changes upcoming in this branch, it becomes useful to do this unconditionally, so it's a good change now.

Log.d(TAG, "NotifyReact: launching main activity")
Log.d(TAG, "android notif: launching main activity")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

android notif: Remove false specificity from a log line

Launching MainActivity doesn't really have anything to do with
"notifying React", i.e., sending information about the opened
notification to the JS environment.

Yeah, the prefix there was really to make this log line identifiably correspond to this line of code -- so it was a reference to the filename, not a description of the substance.

I think the simplest thing is just to delete this line, if you haven't had a use for it. (That's what happens later in the branch anyway.)

(If we were to keep this, I'd drop "android" from the prefix -- this is already going to an Android-specific log system.)

override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: Intent) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

android notif [nfc]: Make MainActivity.onNewIntent's `intent` arg required

ReactActivity's `.onNewIntent`, which we're overriding, requires `intent`.

I think it doesn't. Here's the declaration in ReactActivity.java:

  @Override
  public void onNewIntent(Intent intent) {

That doesn't mention the possibility of the argument being null. But... in Java, all object types are nullable. So Java Intent translates to Kotlin Intent?.

Sometimes there's a Java annotation @NotNull indicating that a given value isn't supposed to be null. But there doesn't seem to be one of those here. So if a caller passes null, no linter would complain either.

If you look in the Kotlin version of the SDK docs:
https://developer.android.com/reference/kotlin/android/app/Activity#onnewintent
this is reflected in the notation Intent!:

protected open fun onNewIntent(intent: Intent!): Unit

The meaning of that ! notation is defined here:
https://kotlinlang.org/docs/java-interop.html#notation-for-platform-types

T! means "T or T?"

Practically, the way I ended up with this version of the definition is probably that I started typing "onNewIntent" and let Android Studio autocomplete an empty override for me.

&& intent.hasExtra(EXTRA_NOTIFICATION_DATA)
) {
val data = intent.getBundleExtra(EXTRA_NOTIFICATION_DATA)
logNotificationData("notif opened", data!!)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

android notif: Move a log line into MainActivity's lifecycle methods

This commit is kind of confusing because it's rather a lot more code than just moving a log line sounds like it should be:
2 files changed, 26 insertions(+), 4 deletions(-)

The added plumbing makes sense because of the real work we have it do in the next commit. I think the clearest thing is probably to squash that one into this. The result is only slightly bigger than this already is:
2 files changed, 26 insertions(+), 6 deletions(-)

and it's easier to understand as changing where we do the real work, with the log line coming along for the ride.


class NotificationIntentService : IntentService("NotificationIntentService") {
override fun onHandleIntent(intent: Intent?) {
val context = applicationContext as? MainApplication ?: return
if (Intent.ACTION_VIEW == intent!!.action) {
val data = intent.getBundleExtra(EXTRA_NOTIFICATION_DATA)
onOpened(context, data!!)
Log.d(TAG, "android notif: launching main activity")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit and the next move this code around twice:

6fd1a19 android notif [nfc]: Inline NotificationUiManager.onOpened
2 files changed, 23 insertions(+), 26 deletions(-)
2521e1d android notif: Remove NotificationIntentService
2 files changed, 21 insertions(+), 50 deletions(-)

If squashed, the resulting diff is just slightly bigger than one of those:
2 files changed, 21 insertions(+), 53 deletions(-)

That also keeps the move within one file, rather than moving to a different file and back. So I think it's clearest to squash those two together.

With that, the preceding commit:
3cf3564 android notif [nfc]: Rename variables to ease next refactor
2 files changed, 6 insertions(+), 6 deletions(-)

also becomes unnecessary.

@gnprice
Copy link
Member

gnprice commented Dec 3, 2021

(Also just replied to the initial thread above.)

chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Dec 7, 2021
…ally

The intent used to start the main activity has the
FLAG_ACTIVITY_NEW_TASK flag. The doc on that flag says this [1]:

> When using this flag, if a task is already running for the
> activity you are now starting, then a new activity will not be
> started; instead, the current task will simply be brought to the
> front of the screen with the state it was last in.

That should mean that there's no hiccup or loss of state if we run
our code to launch the main activity from the main activity when
it's already foregrounded. Empirically that seems to be the case.

I didn't find any history that says the conditional is necessary;
looks like it dates back to the old Wix library we used to use
here [2].

[1] https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_NEW_TASK
[2] zulip#5146 (comment)
@chrisbobbe
Copy link
Contributor Author

Thanks for the review! Revision pushed.

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the revision! Looks good -- just small comments below.


@JvmField
val TAG = "ShareToZulip"

/* Returns true just if we did handle the intent. */
fun maybeHandleIntent(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

android notif [nfc]: Make maybeHandleIntent a helper method on MainActivity

nit: s/ notif//

Similarly for the next commit:

android notif [nfc]: Simplify interface of MainActivity's maybeHandleIntent

Comment on lines 55 to 60
if (!intent.hasExtra(EXTRA_NOTIFICATION_DATA)) {
return false
}

val data = intent.getBundleExtra(EXTRA_NOTIFICATION_DATA)
logNotificationData("notif opened", data!!)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be cleaned up slightly by putting the getBundleExtra first, and then checking if it's null instead of calling hasExtra. Then the !! becomes unnecessary.

That also means that if somehow we get an intent with an EXTRA_NOTIFICATION_DATA extra that isn't a Bundle, then we don't crash at the !! and instead just ignore it here as unhandled.

Comment on lines 80 to 81
// TODO deduplicate this with handleSend in SharingHelper.kt.
// Until then, keep in sync when changing.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could deduplicate all the way down to `appStatus`, except

Yeah. There's still quite a bit of trickiness here that I'd like to deduplicate: sometimes we set a static/global variable, and sometimes we emit an event, and we need to pick the right one or we're likely to drop or duplicate an event. My ideal version of this and handleSend is that they'd each say: here's a WritableMap, and here's a little value identifying where the event should go, and then all that logic like the when (appStatus) would be in a common function they both invoke.

This change is a helpful step! But let's keep these TODO comments here.

Comment on lines 39 to 37
}


val host = (application as ReactApplication).reactNativeHost
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: just one blank line

/* Returns true just if we did handle the intent. */
private fun maybeHandleIntent(
intent: Intent?,
contentResolver: ContentResolver
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parameter can be simplified out in much the same way as application: ReactApplication: the call sites just get it from a getter on this, so this function can do the same thing.

Comment on lines +42 to +38
val host = (application as ReactApplication).reactNativeHost
val reactContext = host.tryGetReactInstanceManager()?.currentReactContext
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, actually I don't love having these lines run here, above the when (intent.action).

The thing is that that means they run even if they're not needed -- if the intent is something else. They look like they could be cheap and side-effect-free… or could not be. I think there isn't any live case where it'd matter even if they are -- the only intents we handle other than the two cases below are the web-auth intents, where we'll want a React instance/context anyway -- but I'd rather not have to worry about that possibility coming up in the future.

Maybe simplest is I'll just add a commit at the end to address this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe simplest is I'll just add a commit at the end to address this.

OK, SGTM, thanks!

chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Dec 8, 2021
…ally

The intent used to start the main activity has the
FLAG_ACTIVITY_NEW_TASK flag. The doc on that flag says this [1]:

> When using this flag, if a task is already running for the
> activity you are now starting, then a new activity will not be
> started; instead, the current task will simply be brought to the
> front of the screen with the state it was last in.

That should mean that there's no hiccup or loss of state if we run
our code to launch the main activity from the main activity when
it's already foregrounded. Empirically that seems to be the case.

I didn't find any history that says the conditional is necessary;
looks like it dates back to the old Wix library we used to use
here [2].

[1] https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_NEW_TASK
[2] zulip#5146 (comment)
@chrisbobbe
Copy link
Contributor Author

Thanks! Revision pushed.

@chrisbobbe
Copy link
Contributor Author

chrisbobbe commented Dec 8, 2021

Thanks! Revision pushed.

Ah, and is there a followup from #5146 (comment) :

actually the cleanest thing there -- which can be a followup -- is probably to parse the outermost parts of the URL.

I'm not really sure what you mean there.

Most of them were already formatted correctly, and they'll still be
formatted correctly at the end of this series.
For the reason given in the TODO, but also because we want to try
removing the me.leolin.shortcutbadger.ShortcutBadger dependency and
seeing if things still work OK.

We leave the `connectGlobal` call because the component still uses
the `dispatch` prop.
…n open

I tested this on the office Android device (a Samsung Galaxy S9
running Android 9), and badge counts still work as I expect,
including decreasing as messages are read, and disappearing if it's
gone down to zero.
We stopped calling one of the methods from JS in a recent commit.
…ally

The intent used to start the main activity has the
FLAG_ACTIVITY_NEW_TASK flag. The doc on that flag says this [1]:

> When using this flag, if a task is already running for the
> activity you are now starting, then a new activity will not be
> started; instead, the current task will simply be brought to the
> front of the screen with the state it was last in.

That should mean that there's no hiccup or loss of state if we run
our code to launch the main activity from the main activity when
it's already foregrounded. Empirically that seems to be the case.

I didn't find any history that says the conditional is necessary;
looks like it dates back to the old Wix library we used to use
here [2].

[1] https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_NEW_TASK
[2] zulip#5146 (comment)
…eact`

This should be fine and NFC because `launchMainActivity` is just
starting an asynchronous task, without waiting for it to complete.
I think we haven't had any use for this line.
And, while we're at it, move launchMainActivity's definition, too,
since it has nothing to do with "notifying React", i.e., sending
information about the opened notification to the JS environment.
To plumb the notification data to the new location, we attach the
data to the launch-main-activity intent as an extra, copied from the
present intent (the one created when the notification was received,
that we're handling with NotificationIntentService).
And replace it with directly opening MainActivity.

When we set targetSdkVersion to 31, we will no longer be allowed to
use a Service as a "notification trampoline", for performance
reasons:
  https://developer.android.com/about/versions/12/behavior-changes-12#notification-trampolines

So, go with their recommended fix, and have the PendingIntent open
MainActivity directly.

In the preceding commits, we moved everything out of
NotificationIntentService except the code that starts MainActivity,
to make this commit as small as possible. (The really important bit
was the `notifyReact` call, which lets the JS environment know about
the notification the user opened.)

Fixes: zulip#5145
And link to a comment from Greg on further work toward
deduplicating.
@gnprice
Copy link
Member

gnprice commented Dec 8, 2021

Thanks! Looks good -- merging. I made one tweak to cut the SuppressLint import in the commit where it stops being used (because Android Studio did that automatically after I made some other edits there.)

I have a draft for that followup and this one: #5146 (comment), and I'll send those as a separate PR because they're nontrivial refactors.

@gnprice gnprice merged commit fdec74f into zulip:main Dec 8, 2021
@chrisbobbe
Copy link
Contributor Author

Great, thanks very much!

@gnprice
Copy link
Member

gnprice commented Dec 8, 2021

#5160

@chrisbobbe chrisbobbe deleted the pr-notif-activity branch December 9, 2021 00:38
sumj25 pushed a commit to sumj25/zulip-mobile that referenced this pull request Jan 12, 2022
…ally

The intent used to start the main activity has the
FLAG_ACTIVITY_NEW_TASK flag. The doc on that flag says this [1]:

> When using this flag, if a task is already running for the
> activity you are now starting, then a new activity will not be
> started; instead, the current task will simply be brought to the
> front of the screen with the state it was last in.

That should mean that there's no hiccup or loss of state if we run
our code to launch the main activity from the main activity when
it's already foregrounded. Empirically that seems to be the case.

I didn't find any history that says the conditional is necessary;
looks like it dates back to the old Wix library we used to use
here [2].

[1] https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_NEW_TASK
[2] zulip#5146 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

android notif: Replace our NotificationIntentService with directly opening MainActivity
2 participants