diff options
Diffstat (limited to 'mobile/android/android-components/docs/_posts')
9 files changed, 584 insertions, 0 deletions
diff --git a/mobile/android/android-components/docs/_posts/2018-07-20-firefox-firetv-browser-session.markdown b/mobile/android/android-components/docs/_posts/2018-07-20-firefox-firetv-browser-session.markdown new file mode 100644 index 0000000000..9b85502ea9 --- /dev/null +++ b/mobile/android/android-components/docs/_posts/2018-07-20-firefox-firetv-browser-session.markdown @@ -0,0 +1,11 @@ +--- +layout: post +title: "📺 Firefox for Fire TV uses browser-session component" +date: 2018-07-30 18:37:00 +0200 +categories: usage +author: sebastian +--- + +As first application [Firefox for Fire TV](https://github.com/mozilla-mobile/firefox-tv) is starting to use the _browser-session_ component. + +[Pull Request](https://github.com/mozilla-mobile/firefox-tv/pull/982)
\ No newline at end of file diff --git a/mobile/android/android-components/docs/_posts/2018-07-30-firefox-notes-fxa.markdown b/mobile/android/android-components/docs/_posts/2018-07-30-firefox-notes-fxa.markdown new file mode 100644 index 0000000000..a8509cf3ee --- /dev/null +++ b/mobile/android/android-components/docs/_posts/2018-07-30-firefox-notes-fxa.markdown @@ -0,0 +1,11 @@ +--- +layout: post +title: "🗒️ Firefox Notes uses services-firefox-accounts component" +date: 2018-07-30 09:48:00 +0200 +categories: usage +author: sebastian +--- + +[Firefox Notes](https://github.com/mozilla/notes/) (A notepad for Firefox) is now using the _service-firefox-accounts_ component for logging users in. It's the first app using this component in production. + +[Pull Request](https://github.com/mozilla/notes/pull/1310) diff --git a/mobile/android/android-components/docs/_posts/2018-08-10-fretboard.md b/mobile/android/android-components/docs/_posts/2018-08-10-fretboard.md new file mode 100644 index 0000000000..f4f852eeb1 --- /dev/null +++ b/mobile/android/android-components/docs/_posts/2018-08-10-fretboard.md @@ -0,0 +1,126 @@ +--- +layout: post +title: "☀️ Google Summer of Code: Fretboard - A/B Testing Framework for Android" +date: 2018-08-10 14:48:00 +0200 +author: fernando +--- + +## Overview +Firefox for Android (codenamed Fennec) started using [KeepSafe's Switchboard library](https://github.com/KeepSafe/Switchboard) and server component in order to run A/B testing or staged rollout of features, but since then the code changed significantly. It used a special [switchboard server](https://github.com/mozilla-services/switchboard-server) that decided which experiments a client is part of and returned a simplified list for the client to consume. However, this required the client to send data (including a unique id) to the server. + +To avoid this Firefox moved to using [Kinto](https://github.com/Kinto/kinto) as storage and server of the experiment configuration. Clients now download the whole list of experiments and decide locally what experiments they are enrolled in (the configuration for Fennec [looks like this](https://firefox.settings.services.mozilla.com/v1/buckets/fennec/collections/experiments/records)). + +The purpose of this Google Summer of Code project, which is called Fretboard, was to develop an A/B testing framework written in Kotlin based on the existing code from Fennec, but making it independent of both the server used and the local storage mechanism, allowing it to be used on other apps, such as Firefox Focus, which started to need to do some A/B tests. + +The source code implemented as part of this project is located at the [android-components](https://github.com/mozilla-mobile/android-components) GitHub repo, more specifically [here](https://github.com/mozilla-mobile/android-components/tree/main/components/service/fretboard) + +## Features +This is a basic and non-exhaustive list of features, for all details you can view the README [here](https://github.com/mozilla-mobile/android-components/blob/main/components/service/fretboard/README.md) + +* Query if a device is part of a specific experiment +* Get experiment associated metadata +* Override a specific experiment (force activate / deactivate it) +* Override device values (such as the appId, version, etc) +* Update experiment list from the server manually or using JobScheduler +* Update experiment list using WorkManager (blocked waiting for a non-alpha version of WorkManager to be released by Google) +* Default source implementation for Kinto + * Uses diff requests to reduce bandwidth usage + * Support for certificate pinning (pending security review) + * Support for validating the signature of the downloaded experiment collection (pending security review) +* Default storage implementation using a flat JSON file + +## Filters +Fretboard allows you to specify the following filters: +- Buckets: Every user is in one of 100 buckets (0-99). For every experiment you can set up a min and max value (0 <= min <= max <= 100). The bounds are [min, max). + - Both max and min are optional. For example, specifying only min = 0 or only max = 100 includes all users + - 0-100 includes all users (as opposed to 0-99) + - 0-0 includes no users (as opposed to just bucket 0) + - 0-1 includes just bucket 0 + - Users will always stay in the same bucket. An experiment targeting 0-25 will always target the same 25% of users +- appId (regex): The app ID (package name) +- version (regex): The app version +- country (regex): country, pulled from the default locale +- lang (regex): language, pulled from the default locale +- device (regex): Android device name +- manufacturer (regex): Android device manufacturer +- region: custom region, different from the one from the default locale (like a GeoIP, or something similar). +- release channel: release channel of the app (alpha, beta, etc) + +## Merged commits +* [Issue #17: Run code quality tools: detekt, ktlint and codecov](https://github.com/mozilla-mobile/android-components/commit/cb4a3685f7ec3d79cf356fba7a95d04f5373c9f7) +* [Remove codecov uploading from taskcluster](https://github.com/mozilla-mobile/android-components/commit/79f49c7a61ee2560bb6e4233586d2b0fc10a978f) +* [Issue #20: Upload test coverage results to codecov](https://github.com/mozilla-mobile/android-components/commit/71a165d094fb777113f06d1e2ac27f2c082bba29) +* [Issue #3: Implement client for loading (partial) experiment configuration from server, Issue #4: Implement storage for saving experiment configuration to disk](https://github.com/mozilla-mobile/android-components/commit/7996c0c11a40939cac73914b83fc2ec578927596) +* [Make experiments variable private and fetch experiments from local storage when updating](https://github.com/mozilla-mobile/android-components/commit/63c9792d9c98b7b5cb12aa62fec792710fa116d0) +* [Make loadExperiments and updateExperiments synchronized and guard against storage file not found](https://github.com/mozilla-mobile/android-components/commit/cd812f3fb63b853d842c7af87c69587bec5f45d6) +* [Issue #5: Schedule frequent updates of experiments configuration](https://github.com/mozilla-mobile/android-components/commit/fcf13f7b794966f2f111ebaccc2a468465c72fff) +* [Issue #6: Implement code for evaluating experiment configuration and bucketing users](https://github.com/mozilla-mobile/android-components/commit/b4d9e41b749f34ac75cdb01e6c5150368fcdcba3) +* [Changed uuid type to String](https://github.com/mozilla-mobile/android-components/commit/b9057672604a675f7d9edddc5d6707e07cef0b78) +* [Add RegionProvider](https://github.com/mozilla-mobile/android-components/commit/af80d363dd7a522f43990550a7096aa5a68c9b3e) +* [Issue #7: Implement simple API for checking if an installation is part of a specific experiment](https://github.com/mozilla-mobile/android-components/commit/93b2fb9407926a7910b353265667938f2daaea63) +* [Issue #29: Add a more Kotlin idiomatic method for checking experiments](https://github.com/mozilla-mobile/android-components/commit/1c7ca693e29356d54f6bf354fe7506aab18dca80) +* [Issue #12: Implement simple API for getting experiment metadata](https://github.com/mozilla-mobile/android-components/commit/36b17e1e2d8d87992dab5b3e8d72e463032a827a) +* [Issue #32: JSONExperimentParserTest is not deterministic](https://github.com/mozilla-mobile/android-components/commit/57d91cfcce0f028a1a3523bc390966f99ad2caa5) +* [Issue #37: Add "export TERM=dumb" to taskcluster script](https://github.com/mozilla-mobile/android-components/commit/0c4ce5d8632c777d535aaeea4678c5cdac42380c) +* [Issue #38: detekt is configured to only run on 'fretboard' module](https://github.com/mozilla-mobile/android-components/commit/d5ebdda10e04d5429adcb0952d2838e38b7d8f89) +* [Issue #41: Add test for IOException on HttpURLConnectionHttpClient](https://github.com/mozilla-mobile/android-components/commit/267c633217b920d2d76d134884e439c7b5efdcb6) +* [Issue #14: Add mechanism for overriding the local experiment configuration](https://github.com/mozilla-mobile/android-components/commit/1f745bcfa8d879145f4514bf431ef9f5eb3978af) +* [Issue #46: Rename AtomicFileExperimentStorage to FlatFileExperimentStorage](https://github.com/mozilla-mobile/android-components/commit/3adeb6c7bc4071f90601b46adc0e418a6edb4828) +* [Issue #53: Change FlatFileExperimentStorage instrumentation tests to use File and remove temp file when done](https://github.com/mozilla-mobile/android-components/commit/e788b0c394ecbe7cfa788bd0628c75d64a85fd6d) +* [Issue #56: Rename FlatFileExperimentStorage package to flatfile](https://github.com/mozilla-mobile/android-components/commit/a824b5d4c0318ce865e3980e0723af6e8c462939) +* [Issue #50: Make FlatFileExperimentStorage receive a File](https://github.com/mozilla-mobile/android-components/commit/2e5103b1984156ad5cf8b3011813894dd25f516f) +* [Issue #432: Fretboard: Kinto delete diffs might lead to crash](https://github.com/mozilla-mobile/android-components/commit/ee164fc8121c1cb338be1e28c00b99d2077a7799) +* [Issue #435: Fretboard: Documentation and guides](https://github.com/mozilla-mobile/android-components/commit/ae40fbeee03009e836455604453bf32678fc06ca) +* [Issue #460: Fretboard: Update documentation](https://github.com/mozilla-mobile/android-components/commit/050b98f41743ccad319bdb8263b1067944971431) +* [Issue #456: Fretboard: Allow filtering by release channel](https://github.com/mozilla-mobile/android-components/commit/43b3938ee9386a33c34f658017f5ae289d2a3e97) +* [Issue #464: Let app access a list of experiments](https://github.com/mozilla-mobile/android-components/commit/5a999c4a482c7b6f6bb7641da9da6031ba4f4568) +* [Issue #466: Fretboard: Move JSONExtensions into support-ktx](https://github.com/mozilla-mobile/android-components/commit/0acf5a5ca79a743d768b5fe80fd04ba35ba5f5ec) +* [Issue #115: Core ping: Report experiments](https://github.com/mozilla-mobile/android-components/commit/0a22a10b5ee7aa081996992aa1567e9abca17105) +* [Issue #485: Remove List.toJSONArray extension method](https://github.com/mozilla-mobile/android-components/commit/c4005e7676050ca19ba8cbd8c7a4c0e37e15073b) +* [Issue #487: Fretboard: Add helper method to get active experiments](https://github.com/mozilla-mobile/android-components/commit/ed825a67c1f54724aa8577bdb4c5874e91968e36) +* [Issue #501: Move JSON extensions to package android.org.json](https://github.com/mozilla-mobile/android-components/commit/a230066b4b568fd2642b27c82f8417afe9abeb54) +* [Issue #504: Fretboard: Require network for JobScheduler-based scheduler](https://github.com/mozilla-mobile/android-components/commit/17630ffd5d8b64c2233a1f3f41f12f6ab73d1cf4) +* [Issue #526: fretboard: README: Explain buckets with examples](https://github.com/mozilla-mobile/android-components/commit/5db98276dd7aa935ab5bec86fba04a40b9ea302a) +* [Issue #524: Fretboard: Add kdoc to Experiment properties](https://github.com/mozilla-mobile/android-components/commit/b8b94f5d421717c0c488656d0838f3478d9f37ad) +* [Issue #542: Fretboard: Remove deleted RegionProvider from README](https://github.com/mozilla-mobile/android-components/commit/6e856a4ddde730058326f2eb4c2441f3c0e8cda9) +* [Issue #541: Fretboard: ExperimentDescriptor should use the experiment name instead of the id](https://github.com/mozilla-mobile/android-components/commit/a9d6ba5863c12411f6b5b9a3cf9bc7dc5900e806) +* [Issue #555: Fretboard: Add test for non HTTP url for HttpURLConnectionHttpClient](https://github.com/mozilla-mobile/android-components/commit/170b8743635f7e6c1cc5b133518d48a86df96271) +* [Issue #557: Fretboard: ExperimentEvaluator: Add tests for empty values and release channel](https://github.com/mozilla-mobile/android-components/commit/7c7bfe00b3bd9192c3b74c53506fa96ff1534dd7) +* [Issue #559: Fretboard: Add tests for ExperimentPayload](https://github.com/mozilla-mobile/android-components/commit/da483522ce9ecfea8b4c2bb00fcd772cbba3537d) +* [Issue #561: Fretboard: Add more tests for Fretboard class](https://github.com/mozilla-mobile/android-components/commit/6aa253a6bb6cd6f898cf96cba9e3973895a54dca) +* [Issue #563: Fretboard: Make kinto properties private](https://github.com/mozilla-mobile/android-components/commit/80d1bbf100448bccca64f1a84c881fc7e1a50795) +* [Issue #565: Fretboard: Handle JSON exceptions on Kinto](https://github.com/mozilla-mobile/android-components/commit/0ba81f51500a063a71b8db35540fb9aeab15218b) +* [Issue #570: Fretboard: More idiomatic Kotlin on FlatFileExperimentStorage and on ExperimentSerializer](https://github.com/mozilla-mobile/android-components/commit/40d000b7a8815cb6b8786e49b501f8e86bee7ef4) +* [Issue #572: Fretboard: Complete kdoc](https://github.com/mozilla-mobile/android-components/commit/c874c343cab18fe64f45ba8eb9a726f829fe06b9) +* [Issue #577: Fretboard: Pass original exception to ExperimentDownloadException](https://github.com/mozilla-mobile/android-components/pull/579) +* [Issue #576: Fretboard: Log ExperimentDownloadException](https://github.com/mozilla-mobile/android-components/pull/582) +* [Issue #590: Fretboard: Blog post](https://github.com/mozilla-mobile/android-components/commit/75a87f8774e2848769e7d18e0253b7bf49464d68) +* [Issue #434: Fretboard: Verify signatures of experiments collection](https://github.com/mozilla-mobile/android-components/commit/f65aad40e6f8ec84009a3f809f816c1c969c7082) + +## Open pull requests +There are two open pull requests. The first one is open pending a review from the security team and the last one is waiting for a non-alpha version of WorkManager to be released by Google: + +1. [Issue #433: Fretboard: Certificate pinning](https://github.com/mozilla-mobile/android-components/pull/446) +2. [Issue #493: Use WorkManager in fretboard](https://github.com/mozilla-mobile/android-components/pull/503) + +## Project progress and difficulties faced +Prior to starting the project, I became familiar with Kinto and the diff response format, as well as with the existing code of the Switchboard fork from Fennec. I also thought it was a good idea to send a pull request to Firefox Focus because this library was going to be integrated into it, and also to become familiar with a code review process at Mozilla. I looked at the issue list and discovered a problem with display cutouts, so I sent two pull requests to address the issue: [the first one](https://github.com/mozilla-mobile/focus-android/pull/2264) and [the second one](https://github.com/mozilla-mobile/focus-android/pull/2278). + +At the beginning of the project it became necessary to familiarize myself with tools like [Taskcluster](https://github.com/taskcluster), which I had never used (although I used similar tools like [Travis CI](https://travis-ci.org/)). + +The most difficult pull requests for me were the ones related to certificate pinning and experiment collection signature verification. For the first one I had to broaden my knowledge about it, as well as research how to properly implement it on Android, avoiding common mistakes. +For the second one the most difficult part was to understand what algorithm Mozilla was using to validate the signatures, and how it worked. I discovered from the Kinto collection `mode` field that it was `p384ecdsa`, and then I had to research how to properly implement it in Kotlin. For this later I needed the help of Julien Vehent and Franziskus Kiefer, which pointed me to a [great talk](https://www.youtube.com/watch?v=b2kPo8YdLTw&t=0s&list=WL&index=2) and also a [Go](https://github.com/mozilla-services/autograph/blob/master/tools/autograph-monitor/contentsignature.go#L35) and [C++](https://searchfox.org/mozilla-central/source/security/manager/ssl/ContentSignatureVerifier.cpp) implementation. After seeing the two implementations I realized my solution wasn't working because I didn't know that the signature actually contained two values concatenated (r and s), which then needed to be encoded using [DER syntax](https://crypto.stackexchange.com/questions/1795/how-can-i-convert-a-der-ecdsa-signature-to-asn-1) + +Overall I think I learned a lot doing this project and I really loved working with Mozilla. + +## Extending Fretboard +Right now there is an [ongoing discussion](https://github.com/mozilla-mobile/android-components/issues/454) about enhancing Fretboard with an expression language (being that JEXL/CEL/etc) for the matchers values instead of regular expressions like it's using now. + +## Thanks to +I would like to thank my mentor [Sebastian Kaspari](https://mozillians.org/en-US/u/sebastian.kaspari/) for all the help and guidance, for being so friendly and available to talk at any moment I needed, as well as reviewing my pull requests quickly. + +I would also like to thank Franziskus Kiefer and Julien Vehent for helping me understand the signature validation system used by Kinto. + +## Related links +* [Summer of Code project](https://summerofcode.withgoogle.com/projects/#6511592707981312) +* [Summer of Code proposal](https://summerofcode.withgoogle.com/serve/6444466999656448/) diff --git a/mobile/android/android-components/docs/_posts/2018-11-05-creating-a-simple-browser.md b/mobile/android/android-components/docs/_posts/2018-11-05-creating-a-simple-browser.md new file mode 100644 index 0000000000..6f459cedae --- /dev/null +++ b/mobile/android/android-components/docs/_posts/2018-11-05-creating-a-simple-browser.md @@ -0,0 +1,9 @@ +--- +layout: post +title: "🌐 Creating a simple browser with Mozilla Android Components." +date: 2018-11-05 14:48:00 +0200 +author: arturo +external_url: https://medium.com/firefox-mobile-engineering/creating-a-simple-browser-with-mozilla-android-components-b33349cde4d0 +--- + +[Published on Medium](https://medium.com/firefox-mobile-engineering/creating-a-simple-browser-with-mozilla-android-components-b33349cde4d0) diff --git a/mobile/android/android-components/docs/_posts/2019-02-19-saving-state.md b/mobile/android/android-components/docs/_posts/2019-02-19-saving-state.md new file mode 100644 index 0000000000..6c7e2eae4a --- /dev/null +++ b/mobile/android/android-components/docs/_posts/2019-02-19-saving-state.md @@ -0,0 +1,109 @@ +--- +layout: post +title: "💾 Saving and restoring browser session state" +date: 2019-02-18 14:35:00 +0200 +author: sebastian +--- + +Losing open tabs in a browser can be a painful experience for the user. By itself [SessionManager](https://mozac.org/api/mozilla.components.browser.session/-session-manager/) keeps its state only in memory. This means that something as simple as switching to a different app can cause Android to [kill the browser app's process and reclaim its resources](https://developer.android.com/guide/components/activities/process-lifecycle). The result of that: The next time the user switches back to the browser app they start with a fresh browser without any tabs open. + +[Mozilla's Android Components](https://mozac.org/) come with two implementations of session storage and helpers to write your own easily. + +## SessionStorage + +The [SessionStorage](https://mozac.org/api/mozilla.components.browser.session.storage/-session-storage/) class that comes with the [browser-session](https://github.com/mozilla-mobile/android-components/tree/main/components/browser/session) component saves the state as a file on disk (using [AtomicFile](https://developer.android.com/reference/android/util/AtomicFile) under the hood). It can be used for a browser that wants to have a single state that gets saved and restored (like Fennec or Chrome). + +```kotlin +val sessionStorage SessionStorage(applicationContext, engine) + +val sessionManager = sessionManager(engine).apply { + sessionStorage.restore()?.let { snapshot -> restore(snapshot) } +} +``` + +ℹ️ Since restoring the last state requires a disk read, it is recommended to perform it off the main thread. This requires the app to gracefully handle the situation where the app starts without any sessions at first. [SessionManager](https://mozac.org/api/mozilla.components.browser.session/-session-manager/) will invoke [onSessionsRestored()](https://mozac.org/api/mozilla.components.browser.session/-session-manager/-observer/on-sessions-restored.html) on a registered [SessionManager.Observer](https://mozac.org/api/mozilla.components.browser.session/-session-manager/-observer/) after restoring has completed. + +## SessionBundleStorage + +Other than [SessionStorage](https://mozac.org/api/mozilla.components.browser.session.storage/-session-storage/) the [SessionBundleStorage](https://mozac.org/api/mozilla.components.feature.session.bundling/-session-bundle-storage/) implementation can save and restore from multiple states. State is saved as a Bundle in a database. + +The storage is set up with a bundle lifetime. [SessionBundleStorage](https://mozac.org/api/mozilla.components.feature.session.bundling/-session-bundle-storage/) will only restore the last bundle if its lifetime has not expired. If there's no active bundle then a new empty bundle will be created to save the state. + +```kotlin +val engine: Engine = ... + +val sessionStorage = SessionBundleStorage( + applicationContext, + bundleLifetime = Pair(1, TimeUnit.HOURS) + +val sessionManager = sessionManager(engine).apply { + // We launch a coroutine on the main thread. Once a snapshot has been restored + // we want to continue with it on the main thread. + GlobalScope.launch(Dispatchers.Main) { + // We restore on the IO dispatcher to not block the main thread: + val snapshot = async(Dispatchers.IO) { + val bundle = sessionStorage.restore() + // If we got a bundle then restore the snapshot from it + bundle.restoreSnapshot(engine) + } + + // If we got a snapshot then restore it now: + snapshot.await()?.let { sessionManager.restore(it) } + } +} +``` + +The storage comes with an [API](https://mozac.org/api/mozilla.components.feature.session.bundling/-session-bundle-storage/#functions) that allows apps to build UIs to list, restore, save and remove bundles. + +![](/assets/images/blog/session-bundles.png) + +## AutoSave + +Knowing when to save state, by calling [SessionStorage.save()](https://mozac.org/api/mozilla.components.browser.session.storage/-session-storage/save.html) or [SessionBundleStorage.save()](https://mozac.org/api/mozilla.components.feature.session.bundling/-session-bundle-storage/save.html), can be complicated. Restoring an outdated state can be an as bad a user experience as restoring no state at all. + +The [AutoSave](https://mozac.org/api/mozilla.components.browser.session.storage/-auto-save/) class is a helper for configuring automatic saving of the browser state - and you can use it with [SessionStorage](https://mozac.org/api/mozilla.components.browser.session.storage/-session-storage/) as well as [SessionBundleStorage](https://mozac.org/api/mozilla.components.feature.session.bundling/-session-bundle-storage/). + +```kotlin +sessionStorage.autoSave(sessionManager) + // Automatically save the state every 30 seconds: + .periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS) + // Save the state when the app goes to the background: + .whenGoingToBackground() + // Save the state whenever sessions change (e.g. a new tab got added or a website + // has completed loading). + .whenSessionsChange() +``` + +## Implementing your own SessionStorage + +If neither [SessionStorage](https://mozac.org/api/mozilla.components.browser.session.storage/-session-storage/) nor [SessionBundleStorage](https://mozac.org/api/mozilla.components.feature.session.bundling/-session-bundle-storage/) satisfy the requirements of your app (e.g. you want to save the state in your favorite database or in a cloud-enabled storage) then it is possible to implement a custom storage. + +The [AutoSave.Storage](https://mozac.org/api/mozilla.components.browser.session.storage/-auto-save/-storage/) interface from the [browser-session](https://github.com/mozilla-mobile/android-components/tree/main/components/browser/session) component defines the methods that are expected from a session storage. Technically it is not required to implement the interface if your app code is the only one interacting with the session store; but implementing the interface makes your implementation compatible with other components code. Specifically you can use [AutoSave](https://mozac.org/api/mozilla.components.browser.session.storage/-auto-save/) with any class implementing SessionStorage without any additional code. + +The [SnapshotSerializer](https://mozac.org/api/mozilla.components.browser.session.storage/-snapshot-serializer/) class is helpful when translating snapshots from and to JSON. + +```kotlin +class MyCustomStorage( + private val engine: Engine +) : AutoSave.Storage { + private val serializer = SnapshotSerializer() + + override fun save(snapshot: SessionManager.Snapshot): Boolean { + val json = serializer.toJSON(snapshot) + + // TODO: Save JSON custom storage. + + // Signal that save operation was successful: + return true + } + + fun restore(): SessionManager.Snapshot { + // TODO: Get JSON from custom storage. + val json = ... + + return serializer.fromJSON(engine, json) + } +} +``` + +ℹ️ For simplicity the implementation above does not handle [JSONException](ad https://developer.android.com/reference/org/json/JSONException.html) which can be thrown by [SnapshotSerializer](https://mozac.org/api/mozilla.components.browser.session.storage/-snapshot-serializer/). diff --git a/mobile/android/android-components/docs/_posts/2019-05-23-deprecation.md b/mobile/android/android-components/docs/_posts/2019-05-23-deprecation.md new file mode 100644 index 0000000000..39d08862d8 --- /dev/null +++ b/mobile/android/android-components/docs/_posts/2019-05-23-deprecation.md @@ -0,0 +1,24 @@ +--- +layout: post +title: "🔚 Deprecating feature-session-bundling, ui-doorhanger and ui-progress" +date: 2019-05-23 14:27:00 +0200 +author: sebastian +--- + +In our upcoming 0.54.0 release we are going to deprecate three of our components: `feature-session-bundling`, `ui-doorhanger` and `ui-progress`. Primary classes of those components have been marked with the `@Deprecated` annotation. With 0.54.0 those components will still be released. However in a future release those components will be removed. + +## feature-session-bundling + +The `feature-session-bundling` provided the functionality for an early feature called "sessions" in the [Fenix project](https://github.com/mozilla-mobile/fenix). This feature has been replaced with "collections" and the functionality for this new feature is provided by the new `feature-tab-collections` component. + +![](/assets/images/blog/session-bundles.png) + +## ui-doorhanger + +This component allowed apps to create "doorhangers" - floating heads-up popup that can be anchored to a view; like in Firefox for Android. This implementation was based on Android's [PopupWindow](https://developer.android.com/reference/android/widget/PopupWindow) class. The implementation caused multiple layout issues and component using it (like `feature-sitepermissions`) switched to using `DialogFragment`s instead. + +## ui-progress + +The `AnimatedProgressBar` was first introduced in Firefox for Android and later used in Firefox Focus and Firefox Lite. A recent performance measurement revealed that the animation of the progress bar can have a negative impact on page load performance. While there was no noticeable difference on the latest high-end devices, on older devices, like a Nexus 5, we saw pages load about ~400ms slower. + +![](/assets/images/blog/progress-performance.png) diff --git a/mobile/android/android-components/docs/_posts/2019-09-02-browser-state.md b/mobile/android/android-components/docs/_posts/2019-09-02-browser-state.md new file mode 100644 index 0000000000..7720f25049 --- /dev/null +++ b/mobile/android/android-components/docs/_posts/2019-09-02-browser-state.md @@ -0,0 +1,64 @@ +--- +layout: post +title: "✨ browser-state: Better state management for apps and components" +date: 2019-09-02 10:30:00 +0200 +author: sebastian +--- + +We have been working on a new component called `browser-state` to eventually replace `browser-session`. Now we are ready to start migrating components from `browser-session` to `browser-state`. This blog posting explains why we want to decommission `browser-session`, describes how `browser-state` works and what our migration plans are. + +## What's the problem with `browser-session`? + +For maintaining the global browser state (e.g. "What tabs are open? What URLs are they pointing to?") the *Android Components* project provides the `browser-session` component. The initial implementation of `browser-session` was a clean, generic re-implementation of what we had developed (more organically) for [Firefox Focus](https://github.com/mozilla-mobile/focus-android). + +In 2018 we [noticed some flaws](https://github.com/mozilla-mobile/android-components/issues/400) in the design of `browser-session`. Those flaws came down to being able to observe the state while being able to modify it at the same time ("mutable state"). This unintended behavior could lead to "event order issues" and observers not really seeing a particular state change. Luckily back then we hadn't seen those issues causing any problems in our apps yet. + +We looked at [multiple ways to prevent those side effects](https://github.com/mozilla-mobile/android-components/pull/453) but that turned out to be almost impossible as long as the state is mutable. After more brainstorming, researching and prototyping we came up with a new design for a completely new component called `browser-state` to eventually replace `browser-session`. + +In 2019 we completed and tweaked the design of the new `browser-state` component until we felt that it was ready to be used in other components. + +## A closer look at `browser-state` + +Concepts used in the `browser-state` component are similar to [Redux](https://redux.js.org/) - a state container library for JavaScript. The Redux documentation is a great way to get familiar with some of the concepts: + * [Core Concepts](https://redux.js.org/introduction/core-concepts) + * [Three Principles](https://redux.js.org/introduction/three-principles) + +### BrowserState + +The global state of the browser is represented by an instance of an immutable data class: `BrowserState` ([API](https://mozac.org/api/mozilla.components.browser.state.state/-browser-state/)). Since it is immutable, an instance of this data class can be observed and processed without any side effects changing it. A state change is represented by the creation of a new `BrowserState` instance. + +### BrowserStore + +The `BrowserStore` ([API](https://mozac.org/api/mozilla.components.browser.state.store/-browser-store/)) is the single source of truth. It holds the current `BrowserState` instance and components, as well as app code, can observe it in order to always receive the latest `BrowserState`. The only way to change the state is by dispatching a `BrowserAction` ([API](https://mozac.org/api/mozilla.components.browser.state.action/-browser-action.html)) on the store. A dispatched `BrowserAction` will be processed internally and a new `BrowserState` object will be emitted by the store. + +## How are we going to migrate apps and components to `browser-state`? + +The `browser-session` component is at the heart of many components and most apps using our components. It is obvious that we cannot migrate all components and apps from `browser-session` to `browser-state` from one *Android Components* release to the next one. Therefore the *Android Components* team made it possible to use `browser-state` and `browser-session` simultaneously and keep the state in both components synchronized. + +```kotlin +val store = BrowserStore() + +// Passing the BrowserStore instance to SessionManager makes sure that both +// components will be kept in sync. +val sessionManager = SessionManager(engine, store) +``` + +With the ability to use both components simultaneously, the *Android Components* team will start migrating components over from `browser-session` to `browser-state`. As part of this work the *Android Components* team will extend and add to `BrowserState` to eventually reach feature parity with the state in `SessionManager` and `Session`. The only thing that may be different for app teams is that some components may require a `BrowserStore` instance instead of a `SessionManager` instance after migration. + +```kotlin +// Before the migration +feature = ToolbarFeature( + layout.toolbar, + components.sessionManager, + components.sessionUseCases.loadUrl, + components.defaultSearchUseCase) + +// After the migration +feature = ToolbarFeature( + layout.toolbar, + components.store, + components.sessionUseCases.loadUrl, + components.defaultSearchUseCase) +``` + +Once the migration of components is largely done, the *Android Components* team will start to help the app teams to plan migrating app code from `browser-session` to `browser-state`. diff --git a/mobile/android/android-components/docs/_posts/2020-05-28-engine-version-feature-flags.md b/mobile/android/android-components/docs/_posts/2020-05-28-engine-version-feature-flags.md new file mode 100644 index 0000000000..c0807397a5 --- /dev/null +++ b/mobile/android/android-components/docs/_posts/2020-05-28-engine-version-feature-flags.md @@ -0,0 +1,54 @@ +--- +layout: post +title: "🚩 Engine version dependent feature flags" +date: 2020-05-28 14:00:00 +0200 +author: sebastian +--- + +When integrating a completely new feature, for example into [Firefox Preview](https://github.com/mozilla-mobile/fenix), this feature may only work with the latest version of an engine, e.g. the latest version of `browser-engine-gecko-nightly`. Manually maintaining a feature flag that gradually gets enabled in build variants as the required functionality becomes available in more stable engine versions (`browser-engine-gecko-beta`, `browser-engine-gecko`) is cumbersome, error-prone and potentially a multi-week long process. + +To help build feature flags, that need to incorporate the engine version, every `Engine` exposes a `version` property. This property is an instance of [`EngineVersion`](https://mozac.org/api/mozilla.components.concept.engine.utils/-engine-version/), which makes it easy to match against specific engine versions. + +## Example + +Let's say you are adding a new feature to [Firefox Preview](https://github.com/mozilla-mobile/fenix). This new feature requires brand new functionality that was just introduced in GeckoView Nightly 77.0. Using [`isAtLeast()`](https://mozac.org/api/mozilla.components.concept.engine.utils/-engine-version/is-at-least.html) you can create a feature flag that will enable this feature in all build variants that are using GeckoView 77.0 or higher. + +```Kotlin +// Enable feature with GeckoView 77+ +val useNewFeature = components.engine.version.isAtLeast(77) +``` + +This also works for minor and patch versions as well as additional metadata that is appended to the version number. + +```Kotlin +// Feature requires GeckoView 77.2 or higher. +val useNewFeature = components.engine.version.isAtLeast(77, 2) +``` + +If needed you can access the individual parts of the version number manually: + +```Kotlin +// For GeckoView (Nightly) 77.0a1 +engine.version.major // 77 +engine.version.minor // 0 +engine.version.patch // 0 +engine.version.metadata // a1 +``` + +### GeckoView vs. WebView + +Note that for GeckoView versions we are using the `MOZILLA_VERSION` that GeckoView exposes (e.g. `78.0a1`) which can be different from version of the maven dependency (e.g. `78.0.20200528032513`). + +In `browser-engine-system`, which is using `WebView`, we are parsing the Chrome version from the [User-Agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent). + +```Kotlin +// Mozilla/5.0 (Linux; Android 10) Build/RPP2.200227.014.A1; wv) +// AppleWebKit/537.36 (KHTML, like Gecko) Version 4.0 +// Chrome/82.0.4062.3 Mobile Safari/537.36 + +// On a device with a WebView with the User-Agent above: +engine.version.major // 82 +engine.version.minor // 0 +engine.version.patch // 4062 +engine.version.metadata // .3 +``` diff --git a/mobile/android/android-components/docs/_posts/2021-07-05-whats-next.md b/mobile/android/android-components/docs/_posts/2021-07-05-whats-next.md new file mode 100644 index 0000000000..43987c3088 --- /dev/null +++ b/mobile/android/android-components/docs/_posts/2021-07-05-whats-next.md @@ -0,0 +1,176 @@ +--- +layout: post +title: "⏭️ After the browser-state migration - what's next?" +date: 2021-07-05 14:00:00 +0200 +author: sebastian +--- + +After 2+ years slowly and incrementally working towards this goal, we [completed the migration](https://github.com/mozilla-mobile/android-components/pull/10436) from the `browser-session` component to the `browser-state` component for state handling. Finally, we were able to delete `browser-session` and all state is now maintained and updated by the [redux](https://redux.js.org/introduction/core-concepts)-like [`BrowserStore`](https://github.com/mozilla-mobile/android-components/blob/main/components/browser/state/src/main/java/mozilla/components/browser/state/store/BrowserStore.kt#L22). + +The following blog posting describes some of the possible follow-up changes to the architecture that we would consider, depending on the outcome of further discussions and prototyping. + +## Reversing the dependency between browser-state and browser-engine implementations + +In the current architecture every `browser-engine` implements `concept-engine` and exposes an abstracted mechanism for observing events. For every `EngineSession` an `EngineObserver` gets created, which will dispatch a `BrowserAction` for every event, updating the centralized state. + +![](/assets/images/blog/engine-architecture.png) + +Now that the migration to `browser-state` is completed, we can reverse this dependency. With a `browser-engine` implementation depending on `browser-state` directly, it can dispatch actions without an observer in between. + +![](/assets/images/blog/simplified-engine-architecture.png) + +This removes the requirement for a shared, abstracted observer interface in `concept-engine`. It would no longer be required to have shared _“glue code”_ for connecting an abstract browser engine with the state handling component. + +A potential downside to this approach is that the `BrowserStore` and related `BrowserAction`s become the new _“interface”_ that a browser engine has to dispatch correctly, which can be harder to understand and follow than simply implementing actual interfaces. + +Overall this seems to be worthwhile exploring and potentially discussing further in an RFC. + +## Jetpack Compose + +Android’s new UI toolkit, [Jetpack Compose](https://developer.android.com/jetpack/compose), will significantly change how we build user-facing features in components and apps. + +The good news is that Jetpack Compose works very nicely with our browser-state component. In Android Components we will provide bindings that allow subscribing to any `lib-state` baked `Store`, causing a recomposition if the observed state changes. + +```kotlin +@Composable +fun SimpleToolbar( + store: BrowserStore +) { + // Subscribe to the URL of the selected tab + val url = store.observeAsState { state -> state.selectedTab?.content?.url } + + // Will automatically get recomposed if the URL changes + Text(url.value ?: "") +} +``` + +### Observing specific tabs + +Today we have many components that optionally take a nullable `tabId: String?` parameter. If a tab ID is provided then the component is supposed to observe this specific tab. And if the parameter is `null` then the component will automatically track the currently selected tab. This has caused issues in the past when a `null` value was provided accidentally, causing the wrong tab to be tracked. With Jetpack Compose we want to make this more explicit and will provide a `Target` class, that lets the caller explicily define what tab should be targeted. In addition to that this allows us to provide extension functions for easily observing this tab. + +```kotlin +@Composable +fun Example(store: BrowserStore) { + // Explicitly observe specific tabs + SimpleToolbar(store, Target.SelectedTab) + SimpleToolbar(store, Target.Tab("tabId")) + SimpleToolbar(store, Target.CustomTab("customTabId")) +} + +@Composable +fun SimpleToolbar( + store: BrowserStore, + target: Target +) { + // Observe the URL of the target. Only when the URL changes, this will + // cause a recomposition. Other changes of the tab get ignored. + val tab: SessionState? by target.observeAsStateFrom( + store = store, + observe = { tab -> tab?.content?.url } + ) + + Text(tab?.content?.url ?: "") +} +``` + +## Observing state directly vs concept components + +A central piece of our [current component architecture](/contributing/architecture) is splitting the implementation into three pieces: A concept component, an implementation, and a glue/feature component for integration. This allows us to easily swap (concept) implementations, without having to change any other code. The downside of this approach is that all implementations need to abide by the interface abstractions. + +Let’s look at the toolbar component as an example. + +![](/assets/images/blog/toolbar-architecture.png) + +* `concept-toolbar` contains the interface and data classes to describe a toolbar and how other components can interact with it. +* `browser-toolbar` is an implementation of concept-toolbar. +* `feature-toolbar` contains the glue code, subscribing to state updates in order to update a toolbar (presenter), and reacting to toolbar events in order to invoke use cases (interactor). + +Using the two concepts above, reversing the dependency and using Jetpack Compose, we can simplify this architecture and reduce it to a single component. A (UI) component written in Jetpack Compose can directly observe `BrowserStore` for state updates and delegate events to a function callback parameter or `UseCase` class directly. + +![](/assets/images/blog/simplified-toolbar-architecture.png) + +## Differently scoped states + +Our browser applications maintain state adhering to three different scopes: browser state, screen state and app state. + +### Browser State + +_"Browser State"_ is the state the browser is in (e.g. “which tabs are open?”) and the state that is shared with our Android Components. It’s available through the `browser-state` component to other components and the application. + +With the bindings mentioned above, `browser-state` works well with Jetpack Compose. + +### Screen State + +_"Screen State"_ is the state for the currently displayed screen (e.g. “what text is the user entering on the search screen?”). In Firefox for Android, we are using `lib-state` backed stores for each screen (e.g. `SearchFragmentStore`). + +As for `browser-state` above, with the Jetpack Compose bindings for every `lib-state` implementation, we can continue to use our existing screen-scoped stores. + +Alternatives, used by the Android community, are [`ViewModel`](https://developer.android.com/reference/kotlin/androidx/lifecycle/ViewModel)s using [`LiveData`](https://developer.android.com/reference/androidx/lifecycle/LiveData) or [`StateFlow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/). Or, at a lower level, Jetpack Compose idioms like [`rememberSaveable()`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/saveable/package-summary#rememberSaveable(kotlin.Array,androidx.compose.runtime.saveable.Saver,kotlin.String,kotlin.Function0)). + +Currently we do not offer bindings in `lib-state` for saving and restoring state to survive activity or process recreation. This is something we could add (based on [`Saver`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/saveable/Saver)), specifically for scoped stores. + +```kotlin +val store = scopedSaveableStore<BrowserScreenState, BrowserScreenAction> { restoredState -> + BrowserScreenStore(restoredState ?: BrowserScreenState()) +} +``` + +### App State + +_"App state"_ is the state the app is in, independently from the currently displayed screen (e.g. “Is the app in light or dark mode?”). + +Currently in Fenix there’s no centralized app state. For some parts there are manager singletons (e.g. `ThemeManager`) or the state is read from `SharedPreferences`. + +We could try using an global app store and the same patterns we use for the browser store and the screen scoped stores. That’s something we are currently trying in Focus ([AppStore](https://github.com/mozilla-mobile/focus-android/blob/main/app/src/main/java/org/mozilla/focus/state/AppStore.kt)). In fact, in Focus, the screen scoped state is a sub state of the application-wide state ([Screen state](https://github.com/mozilla-mobile/focus-android/blob/main/app/src/main/java/org/mozilla/focus/state/AppState.kt#L26)). + +### Stateless composables + +With Jetpack Compose it is preferred to write stateless composables ([State hoisting](https://developer.android.com/jetpack/compose/state#state-hoisting)). This means that state is passed down as function parameters and events are passed up by invoking functions. Listening to the store and dispatching actions sidesteps this mechanism. Only subscribing to state changes at the top layer and passing everything down/up is cumbersome and may introduce a lot of duplicated “glue” code across our projects. + +Let’s look at a simplified example of a browser toolbar. There are two options: + +**Example A**: All state (e.g. the URL to display) gets passed down to the toolbar: + +```Kotlin +@Composable +fun Toolbar( + url: String +) { + Text(url) +} +``` + +**Example B**: The toolbar subscribes to state it needs itself. + +```Kotlin +@Composable +fun Toolbar( + store: BrowserStore +) { + val url = store.observeAsState { state -> state.selectedTab?.content?.url } + Text(url.value ?: "") +} +``` + +The code from example **A** is the most reusable. The app is in full control of what state gets displayed. But this also introduces duplicate code across apps (for getting the state and passing it down) and makes it more likely to introduce bugs (security and spoofing). Example **B** is guaranteed to be consistent across apps. But the composable is strictly tied to the state and how it gets observed. + +When writing (UI) components using Jetpack Compose, we will have to find the right balance between the two patterns. + +In the best case the composition of composables make both patterns possible, depending on the needs of the component consumer: + +```kotlin +@Composable +fun Toolbar( + store: BrowserStore +) { + val url = store.observeAsState { state -> state.selectedTab?.content?.url } + Toolbar(url.value ?: "") +} + +@Composable +fun Toolbar( + url: String +) { + Text(url) +} +``` |