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

Commit 595d04a3 authored by Josh Yang's avatar Josh Yang
Browse files

Introduce a skeleton PlatformAnimationLib-core lib for origin transition.

See go/origin-transition-foundation-lib-impl for the design.
This change creates a new PlatformAnimationLib-core lib which is written
in pure java so that it can be depended by sdk library to support origin
tramsitions.

The main class is OriginTransitionSession, which is a session class
maintaining states for launch/return origin transitions. Clients need
to supply an intent and IRemoteTransition interfaces, and the session
object is responsible for plugging the transition into shell transitions
and calling the corresponding IRemoteTransition to play the animation.

Flag: com.google.android.clockwork.systemui.flags.transitions_enable_origin_transitions_backend
Bug: 347060315
Test: manual test. See a demo in b/347060315#comment6
      atest PlatformAnimationLibCoreTests
Change-Id: I84cbac5959ed83b52434a86acf3155e48583a930
parent caa2c9be
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -33,6 +33,20 @@ java_library {
    ],
}

// This is the core animation library written in java and can be depended by java sdk libraries.
// Please don't introduce kotlin code in this target since kotlin is incompatible with sdk
// libraries.
java_library {
    name: "PlatformAnimationLib-core",
    srcs: [
        "src/com/android/systemui/animation/*.java",
        ":PlatformAnimationLib-aidl",
    ],
    static_libs: [
        "WindowManager-Shell-shared",
    ],
}

filegroup {
    name: "PlatformAnimationLib-aidl",
    srcs: [
+281 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.animation;

import android.annotation.IntDef;
import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.app.ActivityOptions.LaunchCookie;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.RemoteException;
import android.util.Log;
import android.window.IRemoteTransition;
import android.window.RemoteTransition;

import com.android.systemui.animation.shared.IOriginTransitions;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
 * A session object that holds origin transition states for starting an activity from an on-screen
 * UI component and smoothly transitioning back from the activity to the same UI component.
 */
public class OriginTransitionSession {
    private static final String TAG = "OriginTransitionSession";
    static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG);

    @Retention(RetentionPolicy.SOURCE)
    @IntDef(value = {NOT_STARTED, STARTED, CANCELLED})
    private @interface State {}

    @State private static final int NOT_STARTED = 0;
    @State private static final int STARTED = 1;
    @State private static final int CANCELLED = 5;

    private final String mName;
    @Nullable private final IOriginTransitions mOriginTransitions;
    private final Predicate<RemoteTransition> mIntentStarter;
    @Nullable private final IRemoteTransition mEntryTransition;
    @Nullable private final IRemoteTransition mExitTransition;
    private final AtomicInteger mState = new AtomicInteger(NOT_STARTED);

    @Nullable private RemoteTransition mOriginTransition;

    private OriginTransitionSession(
            String name,
            @Nullable IOriginTransitions originTransitions,
            Predicate<RemoteTransition> intentStarter,
            @Nullable IRemoteTransition entryTransition,
            @Nullable IRemoteTransition exitTransition) {
        mName = name;
        mOriginTransitions = originTransitions;
        mIntentStarter = intentStarter;
        mEntryTransition = entryTransition;
        mExitTransition = exitTransition;
        if (hasExitTransition() && !hasEntryTransition()) {
            throw new IllegalArgumentException(
                    "Entry transition must be supplied if you want to play an exit transition!");
        }
    }

    /**
     * Launch the target intent with the supplied entry transition. After this method, the entry
     * transition is expected to receive callbacks. The exit transition will be registered and
     * triggered when the system detects a return from the launched activity to the launching
     * activity.
     */
    public boolean start() {
        if (!mState.compareAndSet(NOT_STARTED, STARTED)) {
            logE("start: illegal state - " + stateToString(mState.get()));
            return false;
        }

        RemoteTransition remoteTransition = null;
        if (hasEntryTransition() && hasExitTransition()) {
            logD("start: starting with entry and exit transition.");
            try {
                remoteTransition =
                        mOriginTransition =
                                mOriginTransitions.makeOriginTransition(
                                        new RemoteTransition(mEntryTransition, mName + "-entry"),
                                        new RemoteTransition(mExitTransition, mName + "-exit"));
            } catch (RemoteException e) {
                logE("Unable to create origin transition!", e);
            }
        } else if (hasEntryTransition()) {
            logD("start: starting with entry transition.");
            remoteTransition = new RemoteTransition(mEntryTransition, mName + "-entry");

        } else {
            logD("start: starting without transition.");
        }
        if (mIntentStarter.test(remoteTransition)) {
            return true;
        } else {
            // Animation is cancelled by intent starter.
            logD("start: cancelled by intent starter!");
            cancel();
            return false;
        }
    }

    /**
     * Cancel the current transition and the registered exit transition if it exists. After this
     * method, this session object can no longer be used. Clients need to create a new session
     * object if they want to launch another intent with origin transition.
     */
    public void cancel() {
        final int lastState = mState.getAndSet(CANCELLED);
        if (lastState == CANCELLED || lastState == NOT_STARTED) {
            return;
        }
        logD("cancel: cancelled transition. Last state: " + stateToString(lastState));
        if (mOriginTransition != null) {
            try {
                mOriginTransitions.cancelOriginTransition(mOriginTransition);
                mOriginTransition = null;
            } catch (RemoteException e) {
                logE("Unable to cancel origin transition!", e);
            }
        }
    }

    private boolean hasEntryTransition() {
        return mEntryTransition != null;
    }

    private boolean hasExitTransition() {
        return mOriginTransitions != null && mExitTransition != null;
    }

    private void logD(String msg) {
        if (DEBUG) {
            Log.d(TAG, "Session[" + mName + "] - " + msg);
        }
    }

    private void logE(String msg) {
        Log.e(TAG, "Session[" + mName + "] - " + msg);
    }

    private void logE(String msg, Throwable e) {
        Log.e(TAG, "Session[" + mName + "] - " + msg, e);
    }

    private static String stateToString(@State int state) {
        switch (state) {
            case NOT_STARTED:
                return "NOT_STARTED";
            case STARTED:
                return "STARTED";
            case CANCELLED:
                return "CANCELLED";
            default:
                return "UNKNOWN(" + state + ")";
        }
    }

    /** A builder to build a {@link OriginTransitionSession}. */
    public static class Builder {
        private final Context mContext;
        @Nullable private final IOriginTransitions mOriginTransitions;
        @Nullable private Supplier<IRemoteTransition> mEntryTransitionSupplier;
        @Nullable private Supplier<IRemoteTransition> mExitTransitionSupplier;
        private String mName;
        @Nullable private Predicate<RemoteTransition> mIntentStarter;

        /** Create a builder that only supports entry transition. */
        public Builder(Context context) {
            this(context, /* originTransitions= */ null);
        }

        /** Create a builder that supports both entry and return transition. */
        public Builder(Context context, @Nullable IOriginTransitions originTransitions) {
            mContext = context;
            mOriginTransitions = originTransitions;
            mName = context.getPackageName();
        }

        /** Specify a name that is used in logging. */
        public Builder withName(String name) {
            mName = name;
            return this;
        }

        /** Specify an intent that will be launched when the session started. */
        public Builder withIntent(Intent intent) {
            return withIntentStarter(
                    transition -> {
                        mContext.startActivity(
                                intent, createDefaultActivityOptions(transition).toBundle());
                        return true;
                    });
        }

        /** Specify a pending intent that will be launched when the session started. */
        public Builder withPendingIntent(PendingIntent pendingIntent) {
            return withIntentStarter(
                    transition -> {
                        try {
                            pendingIntent.send(createDefaultActivityOptions(transition).toBundle());
                            return true;
                        } catch (PendingIntent.CanceledException e) {
                            Log.e(TAG, "Failed to launch pending intent!", e);
                            return false;
                        }
                    });
        }

        private static ActivityOptions createDefaultActivityOptions(
                @Nullable RemoteTransition transition) {
            ActivityOptions options =
                    transition == null
                            ? ActivityOptions.makeBasic()
                            : ActivityOptions.makeRemoteTransition(transition);
            LaunchCookie cookie = new LaunchCookie();
            options.setLaunchCookie(cookie);
            return options;
        }

        /**
         * Specify an intent starter function that will be called to start an activity. The function
         * accepts an optional {@link RemoteTransition} object which can be used to create an {@link
         * ActivityOptions} for the activity launch. The function can also return a {@code false}
         * result to cancel the session.
         *
         * <p>Note: it's encouraged to use {@link #withIntent(Intent)} or {@link
         * #withPendingIntent(PendingIntent)} instead of this method. Use it only if the default
         * activity launch code doesn't satisfy your requirement.
         */
        public Builder withIntentStarter(Predicate<RemoteTransition> intentStarter) {
            mIntentStarter = intentStarter;
            return this;
        }

        /** Add an entry transition to the builder. */
        public Builder withEntryTransition(IRemoteTransition transition) {
            mEntryTransitionSupplier = () -> transition;
            return this;
        }

        /** Add an exit transition to the builder. */
        public Builder withExitTransition(IRemoteTransition transition) {
            mExitTransitionSupplier = () -> transition;
            return this;
        }

        /** Build an {@link OriginTransitionSession}. */
        public OriginTransitionSession build() {
            if (mIntentStarter == null) {
                throw new IllegalArgumentException("No intent, pending intent, or intent starter!");
            }
            return new OriginTransitionSession(
                    mName,
                    mOriginTransitions,
                    mIntentStarter,
                    mEntryTransitionSupplier == null ? null : mEntryTransitionSupplier.get(),
                    mExitTransitionSupplier == null ? null : mExitTransitionSupplier.get());
        }
    }
}
+47 −0
Original line number Diff line number Diff line
// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package {
    default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
    default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
}

android_test {
    name: "PlatformAnimationLibCoreTests",

    defaults: [
        "platform_app_defaults",
    ],
    srcs: [
        "src/**/*.java",
    ],

    dxflags: ["--multi-dex"],
    platform_apis: true,
    test_suites: ["device-tests"],
    static_libs: [
        "PlatformAnimationLib-core",
        "platform-test-rules",
        "testables",
    ],
    compile_multilib: "both",
    libs: [
        "android.test.runner.stubs.system",
        "android.test.base.stubs.system",
    ],

    certificate: "platform",

    manifest: "AndroidManifest.xml",
}
+28 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2024 The Android Open Source Project

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:sharedUserId="android.uid.system"
    package="com.android.systemui.animation.core.tests" >

     <application android:debuggable="true" android:testOnly="true">
         <uses-library android:name="android.test.runner" />
     </application>

    <instrumentation android:name="android.testing.TestableInstrumentation"
        android:targetPackage="com.android.systemui.animation.core.tests"
        android:label="Tests for PlatformAnimationLib-core" />
</manifest>
+39 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2024 The Android Open Source Project
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<configuration description="Runs Tests for PlatformAnimationLib-core.">
    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
        <option name="test-file-name" value="PlatformAnimationLibCoreTests.apk" />
        <option name="install-arg" value="-t" />
    </target_preparer>

    <!-- Among other reasons, root access is needed for screen recording artifacts. -->
    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer">
        <option name="force-root" value="true" />
    </target_preparer>

    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
        <option name="package" value="com.android.systemui.animation.core.tests" />
        <option name="runner" value="android.testing.TestableInstrumentation" />
        <option name="test-filter-dir" value="/data/data/com.android.systemui.animation.core.tests" />
        <option name="hidden-api-checks" value="false"/>
    </test>

    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
        <option name="directory-keys" value="/data/user/0/com.android.systemui.animation.core.tests/files" />
        <option name="collect-on-run-ended-only" value="false" />
    </metrics_collector>
</configuration>
Loading