Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4fa4197a authored by Jake Wharton's avatar Jake Wharton
Browse files

Add Activity non-config instance map-like API

This re-deprecates the single instance non-config instance API. Previously this was deprecated in favor of retained fragments. It was then undeprecated and platform fragments were deprecated (although those two events were not related).

The problem here is that there needs to be a composable mechanism of retaining non-config instances for libraries to use.
This is evidenced in Activity.java itself which already emulates a map internal by using a type and named fields.
The Jetpack library (nee support library) has historically needed this as well, and has been forced to resort to marking the single non-config instance API as final, exposing a differently-named overload, and then coalescing everything into a single type with named fields, just like Activity.java.

This change adds a class-keyed map-like API so that anyone can hook in without being directly in the activity subtype hierarchy.

Unlike the single non-config instance API, the map-like API has a few different behaviors:

- You can store an instance into the map anytime between onCreate and onDestroy (including inside those two lifecycle methods). The old API had a dedicated callback. You can use isChangingConfiguration() in onDestroy to conditionally create the retained instance.
- Stored instances are available for the entire lifetime of the activity instance. The old API cleared the non-config object in onResume.
- By virtue of the previous behavior, instances are retained across multiple config changes without having to re-create the state.

While behaviorally different from the old API, this is more in line with the behavior of retained fragments or non-framework solutions like Jetpack fragments and view models.

A compatibility shim for this in Jetpack will be added.

Bug: 152476567
Test: atest ActivityNonConfigInstanceTest
Change-Id: Ib2484d43327dd52e9cceac1f7201a5b4f694f7bb
parent aefd543e
Loading
Loading
Loading
Loading
+4 −2
Original line number Original line Diff line number Diff line
@@ -3788,13 +3788,14 @@ package android.app {
    method @Nullable public android.view.View getCurrentFocus();
    method @Nullable public android.view.View getCurrentFocus();
    method @Deprecated public android.app.FragmentManager getFragmentManager();
    method @Deprecated public android.app.FragmentManager getFragmentManager();
    method public android.content.Intent getIntent();
    method public android.content.Intent getIntent();
    method @Nullable public Object getLastNonConfigurationInstance();
    method @Deprecated @Nullable public Object getLastNonConfigurationInstance();
    method @NonNull public android.view.LayoutInflater getLayoutInflater();
    method @NonNull public android.view.LayoutInflater getLayoutInflater();
    method @Deprecated public android.app.LoaderManager getLoaderManager();
    method @Deprecated public android.app.LoaderManager getLoaderManager();
    method @NonNull public String getLocalClassName();
    method @NonNull public String getLocalClassName();
    method public int getMaxNumPictureInPictureActions();
    method public int getMaxNumPictureInPictureActions();
    method public final android.media.session.MediaController getMediaController();
    method public final android.media.session.MediaController getMediaController();
    method @NonNull public android.view.MenuInflater getMenuInflater();
    method @NonNull public android.view.MenuInflater getMenuInflater();
    method @Nullable public final <T> T getNonConfigurationInstance(@NonNull Class<T>);
    method public final android.app.Activity getParent();
    method public final android.app.Activity getParent();
    method @Nullable public android.content.Intent getParentActivityIntent();
    method @Nullable public android.content.Intent getParentActivityIntent();
    method public android.content.SharedPreferences getPreferences(int);
    method public android.content.SharedPreferences getPreferences(int);
@@ -3895,7 +3896,7 @@ package android.app {
    method protected void onRestoreInstanceState(@NonNull android.os.Bundle);
    method protected void onRestoreInstanceState(@NonNull android.os.Bundle);
    method public void onRestoreInstanceState(@Nullable android.os.Bundle, @Nullable android.os.PersistableBundle);
    method public void onRestoreInstanceState(@Nullable android.os.Bundle, @Nullable android.os.PersistableBundle);
    method @CallSuper protected void onResume();
    method @CallSuper protected void onResume();
    method public Object onRetainNonConfigurationInstance();
    method @Deprecated @Nullable public Object onRetainNonConfigurationInstance();
    method protected void onSaveInstanceState(@NonNull android.os.Bundle);
    method protected void onSaveInstanceState(@NonNull android.os.Bundle);
    method public void onSaveInstanceState(@NonNull android.os.Bundle, @NonNull android.os.PersistableBundle);
    method public void onSaveInstanceState(@NonNull android.os.Bundle, @NonNull android.os.PersistableBundle);
    method public boolean onSearchRequested(@Nullable android.view.SearchEvent);
    method public boolean onSearchRequested(@Nullable android.view.SearchEvent);
@@ -3919,6 +3920,7 @@ package android.app {
    method public void openOptionsMenu();
    method public void openOptionsMenu();
    method public void overridePendingTransition(int, int);
    method public void overridePendingTransition(int, int);
    method public void postponeEnterTransition();
    method public void postponeEnterTransition();
    method @Nullable public final <T> T putNonConfigurationInstance(@NonNull Class<T>, @Nullable T);
    method public void recreate();
    method public void recreate();
    method public void registerActivityLifecycleCallbacks(@NonNull android.app.Application.ActivityLifecycleCallbacks);
    method public void registerActivityLifecycleCallbacks(@NonNull android.app.Application.ActivityLifecycleCallbacks);
    method public void registerForContextMenu(android.view.View);
    method public void registerForContextMenu(android.view.View);
+104 −5
Original line number Original line Diff line number Diff line
@@ -851,6 +851,7 @@ public class Activity extends ContextThemeWrapper


    static final class NonConfigurationInstances {
    static final class NonConfigurationInstances {
        Object activity;
        Object activity;
        ArrayMap<Class<?>, Object> activityMap;
        HashMap<String, Object> children;
        HashMap<String, Object> children;
        FragmentManagerNonConfig fragments;
        FragmentManagerNonConfig fragments;
        ArrayMap<String, LoaderManager> loaders;
        ArrayMap<String, LoaderManager> loaders;
@@ -859,6 +860,9 @@ public class Activity extends ContextThemeWrapper
    @UnsupportedAppUsage
    @UnsupportedAppUsage
    /* package */ NonConfigurationInstances mLastNonConfigurationInstances;
    /* package */ NonConfigurationInstances mLastNonConfigurationInstances;


    // Stored separately since NonConfigurationInstances are set to null in onResume.
    private ArrayMap<Class<?>, Object> mNonConfigurationInstanceMap;

    @UnsupportedAppUsage
    @UnsupportedAppUsage
    private Window mWindow;
    private Window mWindow;


@@ -3012,8 +3016,12 @@ public class Activity extends ContextThemeWrapper
     * available on older platforms through the Android support libraries.
     * available on older platforms through the Android support libraries.
     *
     *
     * @return the object previously returned by {@link #onRetainNonConfigurationInstance()}
     * @return the object previously returned by {@link #onRetainNonConfigurationInstance()}
     *
     * @deprecated Use {@link #getNonConfigurationInstance} which supports map-like behavior for
     * fetching and retaining multiple objects without requiring a subclass.
     */
     */
    @Nullable
    @Nullable
    @Deprecated
    public Object getLastNonConfigurationInstance() {
    public Object getLastNonConfigurationInstance() {
        return mLastNonConfigurationInstances != null
        return mLastNonConfigurationInstances != null
                ? mLastNonConfigurationInstances.activity : null;
                ? mLastNonConfigurationInstances.activity : null;
@@ -3070,7 +3078,11 @@ public class Activity extends ContextThemeWrapper
     *
     *
     * @return any Object holding the desired state to propagate to the
     * @return any Object holding the desired state to propagate to the
     *         next activity instance
     *         next activity instance
     * @deprecated Use {@link #getNonConfigurationInstance} which supports map-like behavior for
     * fetching and retaining multiple objects without requiring a subclass.
     */
     */
    @Nullable
    @Deprecated
    public Object onRetainNonConfigurationInstance() {
    public Object onRetainNonConfigurationInstance() {
        return null;
        return null;
    }
    }
@@ -3110,8 +3122,91 @@ public class Activity extends ContextThemeWrapper
        return null;
        return null;
    }
    }


    /**
     * Retrieve a non-configuration instance for {@code cls} that was previously associated via a
     * call to {@link #putNonConfigurationInstance}. This method can be called at any time after
     * the initial {@link #onCreate} call, allowing you to extract any useful dynamic state from
     * the previous instance of this activity.
     * <p>
     * Note that the data you retrieve here should <em>only</em> be used as an optimization for
     * handling configuration changes. You should always be able to handle getting a null
     * reference back, and an activity must still be able to restore itself to its previous state
     * (through the normal {@link #onSaveInstanceState(Bundle)} mechanism) even if this method
     * returns null.
     * <p>
     * This functionality is not supported for child activities in an {@link ActivityGroup}.
     */
    @Nullable
    public final <T> T getNonConfigurationInstance(@NonNull Class<T> cls) {
        if (cls == null) {
            throw new NullPointerException("cls == null");
        }
        return cls.cast(mNonConfigurationInstanceMap.get(cls));
    }

    /**
     * Retain {@code instance} across configuration changes when it is known that a new instance of
     * this activity will immediately be created for the new configuration. You can pass any object
     * you like here, which can later be retrieved by calling {@link #getNonConfigurationInstance}
     * in the new activity instance using the same {@code cls}.
     * <p>
     * Instances supplied to this function are used purely as an optimization, and you must not
     * rely on them being available in the new instance. This method can be called at any time
     * during {@link #onCreate} through (and including) {@link #onDestroy}.
     * <p>
     * Each instance is associated with and retrieved by its corresponding {@link Class}. Only one
     * instance can be associated with a class in an activity. If an existing instance is mapped to
     * the supplied class it will be replaced and the old instance returned. Passing null for
     * {@code instance} will remove any stored instance. A return value of null means there was no
     * previous mapping.
     * <p>
     * If you need to dynamically retain instances where a unique class is not available, create a
     * unique class and instance which contains a list, map, or whatever structure facilitates your
     * dynamic setup.
     * <code><pre>
     *   class MyLibraryState {
     *     Map&lt;String, State> keyToState = new ArrayMap&lt;>();
     *   }
     * </pre></code>
     * Similarly, if you need to store something generic like {@code List<String>} you should wrap
     * it in a unique class with no generic parameters.
     * <code><pre>
     *   class MyLibraryState {
     *     List&lt;String> states = new ArrayList&lt;>();
     *   }
     * </pre></code>
     * <p>
     * This API can be used to propagate extensive state from the old to new activity instance, from
     * loaded bitmaps, to network connections, to even actively running threads. Note that you
     * should <em>not</em> propagate any data that may change based on the configuration, including
     * any data loaded from resources such as strings, layouts, or drawables.
     * <p>
     * This functionality is not supported for child activities in an {@link ActivityGroup}.
     *
     * @param cls The class to which {@code instance} will be associated. This will be used to
     * retrieve the instance after configuration change.
     *
     * @param instance The instance associated with {@code cls}.
     *
     * @return The previous instance stored for {@code cls}. This may have come from a previous
     * activity instance or simply a previous call to this method. A return value of null indicates
     * no instance was associated with the specified class.
     */
    @Nullable
    public final <T> T putNonConfigurationInstance(@NonNull Class<T> cls, @Nullable T instance) {
        if (cls == null) {
            throw new NullPointerException("cls == null");
        }
        ArrayMap<Class<?>, Object> map = mNonConfigurationInstanceMap;
        Object removed = instance == null
                ? map.remove(cls)
                : map.put(cls, cls.cast(instance));
        return cls.cast(removed);
    }

    NonConfigurationInstances retainNonConfigurationInstances() {
    NonConfigurationInstances retainNonConfigurationInstances() {
        Object activity = onRetainNonConfigurationInstance();
        Object activity = onRetainNonConfigurationInstance();
        ArrayMap<Class<?>, Object> activityMap = mNonConfigurationInstanceMap;
        HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
        HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
        FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();
        FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();


@@ -3123,13 +3218,9 @@ public class Activity extends ContextThemeWrapper
        mFragments.doLoaderStop(true);
        mFragments.doLoaderStop(true);
        ArrayMap<String, LoaderManager> loaders = mFragments.retainLoaderNonConfig();
        ArrayMap<String, LoaderManager> loaders = mFragments.retainLoaderNonConfig();


        if (activity == null && children == null && fragments == null && loaders == null
                && mVoiceInteractor == null) {
            return null;
        }

        NonConfigurationInstances nci = new NonConfigurationInstances();
        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.activity = activity;
        nci.activity = activity;
        nci.activityMap = activityMap;
        nci.children = children;
        nci.children = children;
        nci.fragments = fragments;
        nci.fragments = fragments;
        nci.loaders = loaders;
        nci.loaders = loaders;
@@ -7922,6 +8013,14 @@ public class Activity extends ContextThemeWrapper
        mParent = parent;
        mParent = parent;
        mEmbeddedID = id;
        mEmbeddedID = id;
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
        if (lastNonConfigurationInstances != null) {
            mNonConfigurationInstanceMap = lastNonConfigurationInstances.activityMap;
        } else {
            // This map is eagerly instantiated because we want people to be able to insert items
            // in onDestroy which is *after* the non-config instances are collected. Being a mutable
            // map, these otherwise-late puts are thus able to still be retained.
            mNonConfigurationInstanceMap = new ArrayMap<>(0);
        }
        if (voiceInteractor != null) {
        if (voiceInteractor != null) {
            if (lastNonConfigurationInstances != null) {
            if (lastNonConfigurationInstances != null) {
                mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
                mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;