Skip to content
This repository has been archived by the owner. It is now read-only.

Issue #929: Use browser-session component for browser state. #982

Merged
merged 1 commit into from Jul 20, 2018

Conversation

@pocmo
Copy link
Contributor

@pocmo pocmo commented Jul 12, 2018

Closes #929: This PR removes the custom session code from the app and replaces it with the browser-session component.

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.

@pocmo
Copy link
Contributor Author

@pocmo pocmo commented Jul 12, 2018

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.

@pocmo
Copy link
Contributor Author

@pocmo pocmo commented Jul 12, 2018

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

@mcomella
Copy link
Contributor

@mcomella mcomella commented Jul 12, 2018

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).

Copy link
Contributor

@mcomella mcomella left a comment

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:

  • Components as a member of ApplicationContext
  • Can we have multiple sessions? We should avoid changing logic around that interaction if it's possible
  • The session observer callbacks not passing in a reference to the changed values
  • How lifecycle owners work for Fragments with view hierarchies

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

This comment has been minimized.

@mcomella

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.

This comment has been minimized.

@pocmo

pocmo Jul 17, 2018
Author Contributor

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) {

This comment has been minimized.

@mcomella

mcomella Jul 16, 2018
Contributor

nit: these didn't change so they don't need to move, right?

This comment has been minimized.

@pocmo

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.

* 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 {

This comment has been minimized.

@mcomella

mcomella Jul 16, 2018
Contributor

nit: private

@@ -18,6 +18,8 @@ class FocusApplication : LocaleAwareApplication() {
lateinit var visibilityLifeCycleCallback: VisibilityLifeCycleCallback
private set

val components by lazy { Components() }

This comment has been minimized.

@mcomella

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. :)

This comment has been minimized.

@pocmo

pocmo Jul 17, 2018
Author Contributor

Is it because we expect it to take the ApplicationContext as an argument in the future? That seems fine then.

Yes, that's exactly the reason. It's going to be Components(applicationContext) because some components will need the application context (#959, #960).

@@ -18,6 +18,8 @@ class FocusApplication : LocaleAwareApplication() {
lateinit var visibilityLifeCycleCallback: VisibilityLifeCycleCallback
private set

val components by lazy { Components() }

This comment has been minimized.

@mcomella

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.

This comment has been minimized.

@pocmo

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.

This comment has been minimized.

@mcomella

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).

This comment has been minimized.

@pocmo

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!


private val hideHandler = FirefoxProgressBarHideHandler(this)
private var session: Session? = null

This comment has been minimized.

@mcomella

mcomella Jul 16, 2018
Contributor

lateinit var? A progress bar shouldn't exist without a session so we should assert that state.

This comment has been minimized.

@pocmo

pocmo Jul 17, 2018
Author Contributor

👍


private val hideHandler = FirefoxProgressBarHideHandler(this)
private var session: Session? = null

This comment has been minimized.

@mcomella

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.

This comment has been minimized.

@pocmo

pocmo Jul 17, 2018
Author Contributor

Yep. Comes with the next release of the components. :)

scheduleHideBar()
}
session = browserFrag.session.also {
it.register(observer = this, owner = browserFrag)

This comment has been minimized.

@mcomella

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?

This comment has been minimized.

@pocmo

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.

}

override fun onLoadingStateChanged() {
session?.let {

This comment has been minimized.

@mcomella

mcomella Jul 16, 2018
Contributor

nit: In this case, to avoid indentation, I prefer:

val session = session ?: return
if (session.isLoading) { ...

This comment has been minimized.

@pocmo

pocmo Jul 17, 2018
Author Contributor

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) }

This comment has been minimized.

@mcomella

mcomella Jul 16, 2018
Contributor

This should be non-null so I think we should assert that.

This comment has been minimized.

@pocmo

pocmo Jul 17, 2018
Author Contributor

👍

@pocmo pocmo force-pushed the pocmo:session branch from 052f470 to 76e14ba Jul 17, 2018
@pocmo
Copy link
Contributor Author

@pocmo pocmo commented Jul 17, 2018

@mcomella Updated the PR and added a separate commit to address your review comments.

To address your review summary:

Components as a member of ApplicationContext

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:
https://github.com/mozilla-mobile/android-components/blob/master/samples/browser/src/main/java/org/mozilla/samples/browser/Components.kt#L28

Can we have multiple sessions? We should avoid changing logic around that interaction if it's possible

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.

The session observer callbacks not passing in a reference to the changed values

As mentioned above: I had the same feeling when using the component here and a patch for that already landed in the components repository.

How lifecycle owners work for Fragments with view hierarchies

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.

@pocmo pocmo force-pushed the pocmo:session branch from 76e14ba to 56bc698 Jul 18, 2018
@pocmo
Copy link
Contributor Author

@pocmo pocmo commented Jul 18, 2018

@mcomella Updated the PR (added the comment to FocusApplication)!

@pocmo pocmo force-pushed the pocmo:session branch from 56bc698 to 53023c2 Jul 18, 2018
@pocmo
Copy link
Contributor Author

@pocmo pocmo commented Jul 18, 2018

... and rebased the PR. Let me know if there are more things. Otherwise I'll squash the commits.

@mcomella
Copy link
Contributor

@mcomella mcomella commented Jul 19, 2018

Sorry, I forgot to get to this today: I'll get to it tomorrow.

Copy link
Contributor

@mcomella mcomella left a comment

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()) {

This comment has been minimized.

@mcomella

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!

This comment has been minimized.

@pocmo

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().

@@ -204,6 +204,11 @@ class MainActivity : LocaleAwareAppCompatActivity(), OnUrlEnteredListener, Media
super.dispatchKeyEvent(event)
}

private fun onNoActiveSession() {

This comment has been minimized.

@mcomella

mcomella Jul 19, 2018
Contributor

nit: you can define this in the session observer (for clearer encapsulation of who wants to call this)

This comment has been minimized.

@pocmo

pocmo Jul 20, 2018
Author Contributor

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?

This comment has been minimized.

@mcomella

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.

This comment has been minimized.

@pocmo

pocmo Jul 20, 2018
Author Contributor

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/"

This comment has been minimized.

@mcomella

mcomella Jul 19, 2018
Contributor

nit: private const val :P I'm not sure what the value of const is though, tbh... compiler note?

This comment has been minimized.

@pocmo

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/";
}
@pocmo pocmo force-pushed the pocmo:session branch from 53023c2 to d382c36 Jul 20, 2018
@pocmo pocmo force-pushed the pocmo:session branch from d382c36 to 49fbf81 Jul 20, 2018
@pocmo
Copy link
Contributor Author

@pocmo pocmo commented Jul 20, 2018

@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.

@pocmo
Copy link
Contributor Author

@pocmo pocmo commented Jul 20, 2018

Taskcluster and travis seem to agree. Landing!

@pocmo pocmo merged commit f627a6d into mozilla-mobile:master Jul 20, 2018
3 checks passed
3 checks passed
Taskcluster (pull_request) TaskGroup: success
Details
codecov/project Absolute coverage decreased by -2.16% but relative coverage increased by +12.56% compared to 0fda8e2
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
@pocmo pocmo deleted the pocmo:session branch Jul 20, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

2 participants