blob: 4767f4f3529b36078d73faea5b87e72175e8d1f4 [file] [log] [blame] [view]
# ActivityResultTracker
## Overview
[`ActivityResultTracker`](https://source.chromium.org/chromium/chromium/src/+/main:ui/android/java/src/org/chromium/ui/base/ActivityResultTracker.java)
is a utility within Chromium for Android designed to more robustly handle
results from activities started for a result. It ensures that results are not
lost even if the base Activity is destroyed and recreated by the Android system
while the launched activity is still active. This is useful for flows like
account addition starting an activity from a different app, where Chrome can be
killed by the OS in the background to free memory during the flow.
## How to use
1. **Implement `ActivityResultTracker.ResultListener`:** Your component that
needs to receive an activity result should implement the
`ActivityResultTracker.ResultListener` interface. This interface has two
methods:
- `onActivityResult(ActivityResult result, @Nullable Bundle savedInstanceData)`:
This is where you handle the result. The `savedInstanceData` parameter
contains the optional data that was saved before starting the activity.
- `getRestorationKey()`: This should return a `String` that uniquely
identifies the _purpose_ of the activity launch across activity
recreations. If multiple indistinguishable instances of your component
must exist in parallel, they can return the same restoration key - an
arbitrary instance will be chosen to handle the activity result in case
the base activity is recreated before the new activity finishes.
```java
class MyComponent implements ActivityResultTracker.ResultListener {
private final String mRestorationKey;
MyComponent(String restorationKey) {
mRestorationKey = restorationKey;
}
@Override
public void onActivityResult(ActivityResult result, @Nullable Bundle savedInstanceData) {
// Handle the result...
}
@Override
public String getRestorationKey() {
return mRestorationKey;
}
}
```
2. **Get an `ActivityResultTracker` Instance:** Get the tracker instance inside
the base Activity and pass it to your component.
```java
ActivityResultTracker tracker = getActivityResultTracker();
```
3. **Register the Listener:** Before you start the activity, register your
activity result listener with the tracker.
```java
MyComponent myComponent = new MyComponent("my_unique_restoration_key");
tracker.register(myComponent);
```
4. **Start the Activity Using the Tracker:**
```java
tracker.startActivity(myComponent, intent, savedInstanceData);
```
The `savedInstanceData` is optional and can be used to save state that needs
to be restored after the base activity's recreation.
**Re-registration is Key:** Components MUST call `tracker.register()` with
the same restoration key in case the base activity is recreated and ensures
it happens automatically. (e.g. it should not be triggered by an user
action) If a pending result for your listener's restoration key already
exists (e.g., from before the activity was recreated), `onActivityResult`
will be called immediately.
If multiple instances of a component were registered using the same
restoration key and a result matching the restoration key is received:
- if it happens after the base activity's recreation, an arbitrary listener
will be called to handle the result, usually the first registered one.
- if it happens without the base activity being killed and restore, the
component used to start the new activity will be called to handle the
result.
5. **Unregister the listener when closing the UI owning it:**
```java
tracker.unregister(myComponent);
```
## The initial problem: lost results on Activity recreation
Chrome mainly relies on
[`IntentRequestTracker`](https://source.chromium.org/chromium/chromium/src/+/main:ui/android/java/src/org/chromium/ui/base/IntentRequestTracker.java;l=23;drc=f4b1a5f91e14941349a0319b35f71d3efb7350af)
owned by
[`WindowAndroid`](https://source.chromium.org/chromium/chromium/src/+/main:ui/android/java/src/org/chromium/ui/base/WindowAndroid.java;l=438;drc=f4b1a5f91e14941349a0319b35f71d3efb7350af)
for activity result handling. This system is straightforward to use, but has one
limitation: if the base Activity hosting the requesting component is killed by
the system (e.g., due to memory pressure) and then later recreated when the user
navigated back, the callback to handle the activity result is lost. This means
the result from the external activity is effectively discarded, breaking the
user flow. This was observed to affect a noticeable percentage (~1% in 2024) of
sign-in attempts, for example.
The standard Android
[`ActivityResultRegistry`](http://go/android-dev/training/basics/intents/result)
API does support handling results across Activity recreation. However, its
limitation is that callbacks must be registered unconditionally during the
Activity/Fragment initialization or `onCreate` to catch the in-flight activity
results after recreation. This model doesn't fit well with Chrome's
architecture, where many features are encapsulated in components (like
Coordinators) that might be created on demand, much later than the Activity's
`onCreate`. These components, often deeply nested, are the ones that actually
need to initiate an activity and handle its result.
## The implemented solution: the Android ActivityResultRegistry & result caching
`ActivityResultTracker` offers to bridge this gap. It's hosted by the base
Activity and uses the `ActivityResultRegistry` from the Android SDK to ensure
result reception across Activity death and recreation. The key features are:
1. **State Persistence:** The tracker saves the restoration keys of all
in-flight activity requests in the Activity's `onSaveInstanceState` bundle.
2. **Internal Re-registration:** On Activity recreation, the tracker uses the
restored keys to re-register internal listeners with the
`ActivityResultRegistry` during `onCreate`. This ensures the tracker catches
any result sent back.
3. **Result Caching:** If a result arrives for a key, and the Chrome component
that originally made the request hasn't yet been recreated and re-registered
its specific callback, the `ActivityResultTracker` holds the
`ActivityResult` and the corresponding restoration key in a temporary cache.
4. **Delayed Delivery:** When the Chrome component (e.g., Coordinator) is
eventually recreated and calls `ActivityResultTracker.register()` with its
restoration key, the tracker checks the cache. If a result is pending for
that key, it's delivered immediately to the newly provided callback
instance.
This design allows components to be created at any time and still reliably
receive activity results, even if the main Activity is destroyed in the
background.
Note that the cached results are not persisted with the recreation, so the
components needs to ensure early registration of the result callback upon
recreation, to prevent the result from being lost during a second activity
recreation.
## Lifecycle integration
The implementation of `ActivityResultTracker` (e.g.,
`ActivityResultTrackerImpl`) requires hooks into the Activity lifecycle:
`onSaveInstanceState`, `onRestoreInstanceState`, and `onDestroy`. These are
handled by the base Activity class (currently `ChromeBaseAppCompatActivity`).