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

Commit eedc8088 authored by Jiaming Liu's avatar Jiaming Liu
Browse files

Do not finish TaskFragment if a new activity is expected

If a TaskFragment only has one activity, and the activity finishes
itself and then immediately launches a new activity, there is a race
condition causing the newly launched activity to be in the TaskFragment
but the TaskFragment gets finished. This is because finish happens
first, causing the SplitController to receive a TaskFragment info with
no running activities in it, and SplitController finishes the
TaskFragment even if a new activity is being launched into the
TaskFragment.

This CL delays the TaskFragment cleanup so that the
activity may appear in the TaskFragment and in this case we do not need
to finish the TaskFragment.

Bug: 390452023
Test: atest SplitControllerTest  ActivityEmbeddingLaunchTests
ActivityEmbeddingLifecycleTests ActivityEmbeddingPolicyTests
Flag: com.android.window.flags.activity_embedding_delay_task_fragment_finish_for_activity_launch

Change-Id: I8dff5d54a5a6cb623e944f7f7ed94d5cff9fbc44
parent 0602ba64
Loading
Loading
Loading
Loading
+16 −5
Original line number Diff line number Diff line
@@ -54,6 +54,8 @@ import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds
import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit;
import static androidx.window.extensions.embedding.TaskFragmentContainer.OverlayContainerRestoreParams;

import static com.android.window.flags.Flags.activityEmbeddingDelayTaskFragmentFinishForActivityLaunch;

import android.annotation.CallbackExecutor;
import android.app.Activity;
import android.app.ActivityClient;
@@ -815,12 +817,18 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
                        .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
                mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
            } else if (!container.isWaitingActivityAppear()) {
                // Do not finish the container before the expected activity appear until
                // timeout.
                if (activityEmbeddingDelayTaskFragmentFinishForActivityLaunch()
                        && container.hasActivityLaunchHint()) {
                    // If we have recently attempted to launch a new activity into this
                    // TaskFragment, we schedule delayed cleanup. If the new activity appears in
                    // this TaskFragment, we no longer need to finish the TaskFragment.
                    container.scheduleDelayedTaskFragmentCleanup();
                } else {
                    mTransactionManager.getCurrentTransactionRecord()
                            .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
                    mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */);
                }
            }
        } else if (wasInPip && isInPip) {
            // No update until exit PIP.
            return;
@@ -3164,6 +3172,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
                    // TODO(b/229680885): skip override launching TaskFragment token by split-rule
                    options.putBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
                            launchedInTaskFragment.getTaskFragmentToken());
                    if (activityEmbeddingDelayTaskFragmentFinishForActivityLaunch()) {
                        launchedInTaskFragment.setActivityLaunchHint();
                    }
                    mCurrentIntent = intent;
                } else {
                    transactionRecord.abort();
+103 −0
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package androidx.window.extensions.embedding;

import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;

import static com.android.window.flags.Flags.activityEmbeddingDelayTaskFragmentFinishForActivityLaunch;

import android.app.Activity;
import android.app.ActivityThread;
import android.app.WindowConfiguration.WindowingMode;
@@ -53,6 +55,8 @@ import java.util.Objects;
class TaskFragmentContainer {
    private static final int APPEAR_EMPTY_TIMEOUT_MS = 3000;

    private static final int DELAYED_TASK_FRAGMENT_CLEANUP_TIMEOUT_MS = 500;

    /** Parcelable data of this TaskFragmentContainer. */
    @NonNull
    private final ParcelableTaskFragmentContainerData mParcelableData;
@@ -165,6 +169,18 @@ class TaskFragmentContainer {
     */
    private boolean mLastDimOnTask;

    /** The timestamp of the latest pending activity launch attempt. 0 means no pending launch. */
    private long mLastActivityLaunchTimestampMs = 0;

    /**
     * The scheduled runnable for delayed TaskFragment cleanup. This is used when the TaskFragment
     * becomes empty, but we expect a new activity to appear in it soon.
     *
     * It should be {@code null} when not scheduled.
     */
    @Nullable
    private Runnable mDelayedTaskFragmentCleanupRunnable;

    /**
     * Creates a container with an existing activity that will be re-parented to it in a window
     * container transaction.
@@ -540,6 +556,10 @@ class TaskFragmentContainer {
            mAppearEmptyTimeout = null;
        }

        if (activityEmbeddingDelayTaskFragmentFinishForActivityLaunch()) {
            clearActivityLaunchHintIfNecessary(mInfo, info);
        }

        mHasCrossProcessActivities = false;
        mInfo = info;
        if (mInfo == null || mInfo.isEmpty()) {
@@ -1064,6 +1084,89 @@ class TaskFragmentContainer {
        return isOverlay() && mParcelableData.mAssociatedActivityToken != null;
    }

    /**
     * Indicates whether there is possibly a pending activity launching into this TaskFragment.
     *
     * This should only be used as a hint because we cannot reliably determine if the new activity
     * is going to appear into this TaskFragment.
     *
     * TODO(b/293800510) improve activity launch tracking in TaskFragment.
     */
    boolean hasActivityLaunchHint() {
        if (mLastActivityLaunchTimestampMs == 0) {
            return false;
        }
        if (System.currentTimeMillis() > mLastActivityLaunchTimestampMs + APPEAR_EMPTY_TIMEOUT_MS) {
            // The hint has expired after APPEAR_EMPTY_TIMEOUT_MS.
            mLastActivityLaunchTimestampMs = 0;
            return false;
        }
        return true;
    }

    /** Records the latest activity launch attempt. */
    void setActivityLaunchHint() {
        mLastActivityLaunchTimestampMs = System.currentTimeMillis();
    }

    /**
     * If we get a new info showing that the TaskFragment has more activities than the previous
     * info, we clear the new activity launch hint.
     *
     * Note that this is not a reliable way and cannot cover situations when the attempted
     * activity launch did not cause TaskFragment info activity count changes, such as trampoline
     * launches or single top launches.
     *
     * TODO(b/293800510) improve activity launch tracking in TaskFragment.
     */
    private void clearActivityLaunchHintIfNecessary(
            @Nullable TaskFragmentInfo oldInfo, @NonNull TaskFragmentInfo newInfo) {
        final int previousActivityCount = oldInfo == null ? 0 : oldInfo.getRunningActivityCount();
        if (newInfo.getRunningActivityCount() > previousActivityCount) {
            mLastActivityLaunchTimestampMs = 0;
            cancelDelayedTaskFragmentCleanup();
        }
    }

    /**
     * Schedules delayed TaskFragment cleanup due to pending activity launch. The scheduled cleanup
     * will be canceled if a new activity appears in this TaskFragment.
     */
    void scheduleDelayedTaskFragmentCleanup() {
        if (mDelayedTaskFragmentCleanupRunnable != null) {
            // Remove the previous callback if there is already one scheduled.
            mController.getHandler().removeCallbacks(mDelayedTaskFragmentCleanupRunnable);
        }
        mDelayedTaskFragmentCleanupRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mController.mLock) {
                    if (mDelayedTaskFragmentCleanupRunnable != this) {
                        // The scheduled cleanup runnable has been canceled or rescheduled, so
                        // skipping.
                        return;
                    }
                    if (isEmpty()) {
                        mLastActivityLaunchTimestampMs = 0;
                        mController.onTaskFragmentAppearEmptyTimeout(
                                TaskFragmentContainer.this);
                    }
                    mDelayedTaskFragmentCleanupRunnable = null;
                }
            }
        };
        mController.getHandler().postDelayed(
                mDelayedTaskFragmentCleanupRunnable, DELAYED_TASK_FRAGMENT_CLEANUP_TIMEOUT_MS);
    }

    private void cancelDelayedTaskFragmentCleanup() {
        if (mDelayedTaskFragmentCleanupRunnable == null) {
            return;
        }
        mController.getHandler().removeCallbacks(mDelayedTaskFragmentCleanupRunnable);
        mDelayedTaskFragmentCleanupRunnable = null;
    }

    @Override
    public String toString() {
        return toString(true /* includeContainersToFinishOnExit */);