Issue #929: Use browser-session component for browser state. #982
Conversation
2165d46
to
052f470
|
Codecov: Unfortunately the coverage went down because there were some tests for the removed code. On the positive side the components have a good/better coverage. :) Sputnik: That's something we saw in other repos too: If we remove files then sputnik fails because it cannot look at those files (obviously). This seems to be a sputnik bug. |
Filed an issue for that: TouK/sputnik-ci#23 |
|
I'm trying to land some MediaSession stuff which needs to get uplifted to v2.2 so I'm going to review that after the core work is done (which should be today or tomorrow). |
|
Looks pretty good but there are a few areas I'm concerned about that I think we should discuss the best approach on. The detailed comments are inline but to highlight these areas:
This is a long PR and GH doesn't have interdiff so I'd prefer if you can make your changes as separate commits and squash at the end! |
| @@ -135,7 +136,7 @@ internal class FirefoxAmazonWebView( | |||
| super.saveState(stateData) | |||
| client.saveState(this, stateData) | |||
|
|
|||
| session.saveWebViewState(stateData) | |||
| session.webViewState = stateData | |||
mcomella
Jul 16, 2018
Contributor
nit: Should this be Session.savedWebViewState or cachedWebViewState? Without a qualifier, it's unclear how the session's webViewState should interact with the applications.
nit: Should this be Session.savedWebViewState or cachedWebViewState? Without a qualifier, it's unclear how the session's webViewState should interact with the applications.
pocmo
Jul 17, 2018
Author
Contributor
Okay, I'll rename it to savedWebViewState.
Okay, I'll rename it to savedWebViewState.
| @@ -254,5 +244,13 @@ public boolean isYoutubeTV() { | |||
| // TODO: Default implementation | |||
| return false; | |||
| } | |||
|
|
|||
| @Override | |||
| public void saveWebViewState(@NonNull Session session) { | |||
mcomella
Jul 16, 2018
Contributor
nit: these didn't change so they don't need to move, right?
nit: these didn't change so they don't need to move, right?
pocmo
Jul 17, 2018
Author
Contributor
Correct! I asked Android Studio to add the methods from the interface (different Session) and they ended up there. I'll move them back.
Correct! I asked Android Studio to add the methods from the interface (different Session) and they ended up there. I'll move them back.
| * We are not using an "Engine" implementation yet. Therefore we create this dummy that we pass to | ||
| * the <code>SessionManager</code> for now. | ||
| */ | ||
| class DummyEngine : Engine { |
mcomella
Jul 16, 2018
Contributor
nit: private
nit: private
| @@ -18,6 +18,8 @@ class FocusApplication : LocaleAwareApplication() { | |||
| lateinit var visibilityLifeCycleCallback: VisibilityLifeCycleCallback | |||
| private set | |||
|
|
|||
| val components by lazy { Components() } | |||
mcomella
Jul 16, 2018
Contributor
What's the value in tying this to the ApplicationContext rather than just making it static, i.e. object Components? I'd prefer to avoid adding more objects to Context because 1) we're tying something to the Context's lifecycle which has no need to be tied to it and 2) Context is already a god object with too many objects attached to it and I'd prefer not to add more.
Is it because we expect it to take the ApplicationContext as an argument in the future? That seems fine then.
Is it so we can mock more easily (by mocking the ApplicationContext)? In this case, I have other ideas but won't waste time typing them right now if it's not relevant. :)
What's the value in tying this to the ApplicationContext rather than just making it static, i.e. object Components? I'd prefer to avoid adding more objects to Context because 1) we're tying something to the Context's lifecycle which has no need to be tied to it and 2) Context is already a god object with too many objects attached to it and I'd prefer not to add more.
Is it because we expect it to take the ApplicationContext as an argument in the future? That seems fine then.
Is it so we can mock more easily (by mocking the ApplicationContext)? In this case, I have other ideas but won't waste time typing them right now if it's not relevant. :)
| @@ -18,6 +18,8 @@ class FocusApplication : LocaleAwareApplication() { | |||
| lateinit var visibilityLifeCycleCallback: VisibilityLifeCycleCallback | |||
| private set | |||
|
|
|||
| val components by lazy { Components() } | |||
mcomella
Jul 16, 2018
Contributor
nit: I see no value in making this lazy: we're guaranteed to use this. I tend to avoid lazy computation unless it's explicitly needed because if you chain up enough lazy calls, it becomes hard to predict the performance of your program and the lazy calls sneak in slowly over time.
nit: I see no value in making this lazy: we're guaranteed to use this. I tend to avoid lazy computation unless it's explicitly needed because if you chain up enough lazy calls, it becomes hard to predict the performance of your program and the lazy calls sneak in slowly over time.
pocmo
Jul 17, 2018
Author
Contributor
It's lazy because at the time FocusApplication gets created it's not a valid Context yet that can be used as a Context for various things. So I delay the creation until it's actually needed.
It's lazy because at the time FocusApplication gets created it's not a valid Context yet that can be used as a Context for various things. So I delay the creation until it's actually needed.
mcomella
Jul 17, 2018
Contributor
I think I'd prefer lateinit with initialization in onCreate: that way it's explicit after which point it's intended to be used.
But if you prefer lazy, please add a comment explaining why it's necessary (not a valid Context yet).
I think I'd prefer lateinit with initialization in onCreate: that way it's explicit after which point it's intended to be used.
But if you prefer lazy, please add a comment explaining why it's necessary (not a valid Context yet).
pocmo
Jul 18, 2018
Author
Contributor
lateinit works too. But the downside of that is that I would need to make the components property mutable and I'm not sure that's really better. FWIW app code can always use the property. Just the opposite is not true: Components can't use FocusApplication (as context) until it has gone through the lifecycle methods. I'll add a comment!
lateinit works too. But the downside of that is that I would need to make the components property mutable and I'm not sure that's really better. FWIW app code can always use the property. Just the opposite is not true: Components can't use FocusApplication (as context) until it has gone through the lifecycle methods. I'll add a comment!
|
|
||
| private val hideHandler = FirefoxProgressBarHideHandler(this) | ||
| private var session: Session? = null |
mcomella
Jul 16, 2018
Contributor
lateinit var? A progress bar shouldn't exist without a session so we should assert that state.
lateinit var? A progress bar shouldn't exist without a session so we should assert that state.
pocmo
Jul 17, 2018
Author
Contributor
👍
|
|
||
| private val hideHandler = FirefoxProgressBarHideHandler(this) | ||
| private var session: Session? = null |
mcomella
Jul 16, 2018
Contributor
fwiw, if the observers passed in the value they change (i.e. isLoading, url), we wouldn't need to keep a reference to session and potentially mismanage it.
fwiw, if the observers passed in the value they change (i.e. isLoading, url), we wouldn't need to keep a reference to session and potentially mismanage it.
pocmo
Jul 17, 2018
Author
Contributor
Yep. Comes with the next release of the components. :)
Yep. Comes with the next release of the components. :)
| scheduleHideBar() | ||
| } | ||
| session = browserFrag.session.also { | ||
| it.register(observer = this, owner = browserFrag) |
mcomella
Jul 16, 2018
Contributor
Do lifecycleOwners work correctly with fragment's view hierarchies? e.g. A fragment creates a view hierarchy, removes the view hierarchy, and adds another one, what happens? Do we still receive events to the listeners of the invalid view hierarchy? Are those objects prevented from being GC'd?
Do lifecycleOwners work correctly with fragment's view hierarchies? e.g. A fragment creates a view hierarchy, removes the view hierarchy, and adds another one, what happens? Do we still receive events to the listeners of the invalid view hierarchy? Are those objects prevented from being GC'd?
pocmo
Jul 17, 2018
Author
Contributor
This works the same as the observe() on the LiveData in the code before: The observer stays registered until the lifecycle of the owner changes to DESTROYED. So it's at least not a regression.
Attaching observers to the "lifecycle" of a View is something that is so common that we added something for that too. We can attach it to FirefoxProgressBar instead of the Fragment. I'll update the PR.
This works the same as the observe() on the LiveData in the code before: The observer stays registered until the lifecycle of the owner changes to DESTROYED. So it's at least not a regression.
Attaching observers to the "lifecycle" of a View is something that is so common that we added something for that too. We can attach it to FirefoxProgressBar instead of the Fragment. I'll update the PR.
| } | ||
|
|
||
| override fun onLoadingStateChanged() { | ||
| session?.let { |
mcomella
Jul 16, 2018
Contributor
nit: In this case, to avoid indentation, I prefer:
val session = session ?: return
if (session.isLoading) { ...
nit: In this case, to avoid indentation, I prefer:
val session = session ?: return
if (session.isLoading) { ...
pocmo
Jul 17, 2018
Author
Contributor
With the lateinit change session is not nullable anymore. I'l update that.
With the lateinit change session is not nullable anymore. I'l update that.
| this.sessionIsLoadingObserver = sessionIsLoadingObserver | ||
| } | ||
|
|
||
| fun onDestroyWebView(webView: IWebView, session: Session) { | ||
| webView.removeJavascriptInterface(JS_INTERFACE_IDENTIFIER) | ||
| this.webView = null | ||
|
|
||
| session.loading.removeObserver(sessionIsLoadingObserver!!) | ||
| sessionIsLoadingObserver?.let { session.unregister(it) } |
mcomella
Jul 16, 2018
Contributor
This should be non-null so I think we should assert that.
This should be non-null so I think we should assert that.
pocmo
Jul 17, 2018
Author
Contributor
👍
|
@mcomella Updated the PR and added a separate commit to address your review comments. To address your review summary:
As mentioned in the comments: Components will need application context going forward. Especially for #959 and #960. We use a similar concept in our sample browser:
Yes, I changed everything back to handle multiple sessions. Let's address that separately. We are also looking into making single session use cases easier in the components.
As mentioned above: I had the same feeling when using the component here and a patch for that already landed in the components repository.
This was a 1:1 conversion from the previous code where the LiveData observer was bound to the fragment too. I updated this code to bind the observer to the view instead of the Fragment. |
|
@mcomella Updated the PR (added the comment to FocusApplication)! |
|
... and rebased the PR. Let me know if there are more things. Otherwise I'll squash the commits. |
|
Sorry, I forgot to get to this today: I'll get to it tomorrow. |
|
lgtm w/ comments addressed. Thanks! Don't forget to squash! :) |
| @@ -53,13 +53,13 @@ class MainActivity : LocaleAwareAppCompatActivity(), OnUrlEnteredListener, Media | |||
| } | |||
|
|
|||
| override fun onSessionRemoved(session: Session) { | |||
| // There's no active session. Start a new session with "homepage". | |||
| ScreenController.showBrowserScreenForUrl(this@MainActivity, supportFragmentManager, APP_URL_HOME, Source.NONE) | |||
| if (components.sessionManager.sessions.isEmpty()) { | |||
mcomella
Jul 19, 2018
Contributor
The current logic is:
if (sessionsIsEmpty)
showBrowserScreenForHome()
else
showBrowserScreenForCurrentUrl()
The logic in this method isn't the same. Is the logic the same because onSessionSelected will be called if onSessionRemoved is called? If so, this is good; if not, please fix it!
The current logic is:
if (sessionsIsEmpty)
showBrowserScreenForHome()
else
showBrowserScreenForCurrentUrl()The logic in this method isn't the same. Is the logic the same because onSessionSelected will be called if onSessionRemoved is called? If so, this is good; if not, please fix it!
pocmo
Jul 20, 2018
Author
Contributor
Is the logic the same because onSessionSelected will be called if onSessionRemoved is called?
Yes! If the removed session was the selected session then SessionManager will determine the next selected session (like in Fennec) and will notify observers via onSessionSelected().
Is the logic the same because onSessionSelected will be called if onSessionRemoved is called?
Yes! If the removed session was the selected session then SessionManager will determine the next selected session (like in Fennec) and will notify observers via onSessionSelected().
| @@ -204,6 +204,11 @@ class MainActivity : LocaleAwareAppCompatActivity(), OnUrlEnteredListener, Media | |||
| super.dispatchKeyEvent(event) | |||
| } | |||
|
|
|||
| private fun onNoActiveSession() { | |||
mcomella
Jul 19, 2018
Contributor
nit: you can define this in the session observer (for clearer encapsulation of who wants to call this)
nit: you can define this in the session observer (for clearer encapsulation of who wants to call this)
pocmo
Jul 20, 2018
Author
Contributor
I'll update.
I'll update.
| @@ -12,5 +12,5 @@ import org.mozilla.focus.Components | |||
| /** | |||
| * Get the components of this application. | |||
| */ | |||
| val Fragment.components: Components | |||
| get() = context!!.components | |||
| val Fragment.components: Components? | |||
mcomella
Jul 19, 2018
Contributor
nit: I still don't love that it's unclear why Fragment.components can be null - that it's tied to the Context. I think I might actually prefer it to be val Context.components: Components (i.e. non-optional) to make it clear that it's the context we need to look out for.
nit: I still don't love that it's unclear why Fragment.components can be null - that it's tied to the Context. I think I might actually prefer it to be val Context.components: Components (i.e. non-optional) to make it clear that it's the context we need to look out for.
pocmo
Jul 20, 2018
Author
Contributor
Okay, I removed this extension completely and access components via a Context in the fragments now.
Okay, I removed this extension completely and access components via a Context in the fragments now.
| @@ -12,6 +12,8 @@ import org.mockito.Mockito.mock | |||
| import org.mozilla.focus.iwebview.IWebView | |||
| import org.robolectric.RobolectricTestRunner | |||
|
|
|||
| private val TEST_URL = "https://github.com/mozilla-mobile/focus-android/" | |||
mcomella
Jul 19, 2018
Contributor
nit: private const val :P I'm not sure what the value of const is though, tbh... compiler note?
nit: private const val :P I'm not sure what the value of const is though, tbh... compiler note?
pocmo
Jul 20, 2018
Author
Contributor
Was curious and decompiled to Java:
WIthout const:
public final class SessionCallbackProxyTestKt {
private static final String TEST_URL = "https://github.com/mozilla-mobile/focus-android/";
// $FF: synthetic method
@NotNull
public static final String access$getTEST_URL$p() {
return TEST_URL;
}
}
With const:
public final class SessionCallbackProxyTestKt {
private static final String TEST_URL = "https://github.com/mozilla-mobile/focus-android/";
}
Was curious and decompiled to Java:
WIthout const:
public final class SessionCallbackProxyTestKt {
private static final String TEST_URL = "https://github.com/mozilla-mobile/focus-android/";
// $FF: synthetic method
@NotNull
public static final String access$getTEST_URL$p() {
return TEST_URL;
}
}With const:
public final class SessionCallbackProxyTestKt {
private static final String TEST_URL = "https://github.com/mozilla-mobile/focus-android/";
}|
@mcomella Adressed your last comments. Updated and squashed the commits. Rebased. And just tested again a build of that a bit on a Fire TV 4k stick. I think that's ready to merge now. |
|
|
Closes #929: This PR removes the custom session code from the app and replaces it with the
browser-sessioncomponent.There are three missing pieces in the component (webview state, session source, blocking state) that I added via property extensions for now. I created upstream issues to get that into the component and once that has happened we can remove the extensions here.
For now this is almost a 1:1 replacement. In further iterations we can start to clean some things up. I want to look into how we can make usage of the component for "single-session" users a bit easier.
I did test this on a Fire TV 4k stick a bit. I seems to work fine. However as this touches a lot of areas I think this should get good test coverage by QA too.