diff --git a/compatLib/src/main/java/android/view/InsetsSource.java b/compatLib/src/main/java/android/view/InsetsSource.java new file mode 100644 index 0000000000..71f0516408 --- /dev/null +++ b/compatLib/src/main/java/android/view/InsetsSource.java @@ -0,0 +1,692 @@ +/* + * Copyright (C) 2018 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 android.view; + +import static android.view.InsetsSourceProto.FRAME; +import static android.view.InsetsSourceProto.TYPE; +import static android.view.InsetsSourceProto.TYPE_NUMBER; +import static android.view.InsetsSourceProto.VISIBLE; +import static android.view.InsetsSourceProto.VISIBLE_FRAME; +import static android.view.WindowInsets.Type.captionBar; +import static android.view.WindowInsets.Type.ime; + +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Insets; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.proto.ProtoOutputStream; +import android.view.WindowInsets.Type.InsetsType; +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; +import java.util.StringJoiner; + +/** + * Represents the state of a single entity generating insets for clients. + * + * @hide + */ +public class InsetsSource implements Parcelable { + + @Retention(RetentionPolicy.SOURCE) + @IntDef( + prefix = "SIDE_", + value = {SIDE_NONE, SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM, SIDE_UNKNOWN}) + public @interface InternalInsetsSide {} + + static final int SIDE_NONE = 0; + static final int SIDE_LEFT = 1; + static final int SIDE_TOP = 2; + static final int SIDE_RIGHT = 3; + static final int SIDE_BOTTOM = 4; + static final int SIDE_UNKNOWN = 5; + + /** The insets source ID of IME */ + public static final int ID_IME = createId(null, 0, ime()); + + /** The insets source ID of the IME caption bar ("fake" IME navigation bar). */ + public static final int ID_IME_CAPTION_BAR = + InsetsSource.createId(null /* owner */, 1 /* index */, captionBar()); + + /** + * Controls whether this source suppresses the scrim. If the scrim is ignored, the system won't + * draw a semi-transparent scrim behind the system bar area even when the bar contrast is + * enforced. + * + * @see android.R.styleable#Window_enforceStatusBarContrast + * @see android.R.styleable#Window_enforceNavigationBarContrast + */ + public static final int FLAG_SUPPRESS_SCRIM = 1; + + /** + * Controls whether the insets frame will be used to move {@link RoundedCorner} inward with the + * insets frame size when calculating the rounded corner insets to other windows. + * + *

For example, task bar will draw fake rounded corners above itself, so we need to move the + * rounded corner up by the task bar insets size to make other windows see a rounded corner + * above the task bar. + */ + public static final int FLAG_INSETS_ROUNDED_CORNER = 1 << 1; + + /** Controls whether the insets provided by this source should be forcibly consumed. */ + public static final int FLAG_FORCE_CONSUMING = 1 << 2; + + /** Controls whether the insets source will play an animation when resizing. */ + public static final int FLAG_ANIMATE_RESIZING = 1 << 3; + + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + prefix = "FLAG_", + value = { + FLAG_SUPPRESS_SCRIM, + FLAG_INSETS_ROUNDED_CORNER, + FLAG_FORCE_CONSUMING, + FLAG_ANIMATE_RESIZING, + }) + public @interface Flags {} + + /** + * Used when there are no bounding rects to describe an inset, which is only possible when the + * insets itself is {@link Insets#NONE}. + */ + private static final Rect[] NO_BOUNDING_RECTS = new Rect[0]; + + private @Flags int mFlags; + + /** An unique integer to identify this source across processes. */ + private final int mId; + + private final @InsetsType int mType; + + /** Frame of the source in screen coordinate space */ + private final Rect mFrame; + + private @Nullable Rect mVisibleFrame; + private @Nullable Rect[] mBoundingRects; + + private boolean mVisible; + + /** + * Used to decide which side of the relative frame should receive insets when the frame fully + * covers the relative frame. + */ + private @InternalInsetsSide int mSideHint = SIDE_NONE; + + private final Rect mTmpFrame = new Rect(); + private final Rect mTmpBoundingRect = new Rect(); + + public InsetsSource(int id, @InsetsType int type) { + mId = id; + mType = type; + mFrame = new Rect(); + mVisible = (WindowInsets.Type.defaultVisible() & type) != 0; + } + + public InsetsSource(InsetsSource other) { + mId = other.mId; + mType = other.mType; + mFrame = new Rect(other.mFrame); + mVisible = other.mVisible; + mVisibleFrame = other.mVisibleFrame != null ? new Rect(other.mVisibleFrame) : null; + mFlags = other.mFlags; + mSideHint = other.mSideHint; + mBoundingRects = other.mBoundingRects != null ? other.mBoundingRects.clone() : null; + } + + public void set(InsetsSource other) { + mFrame.set(other.mFrame); + mVisible = other.mVisible; + mVisibleFrame = other.mVisibleFrame != null ? new Rect(other.mVisibleFrame) : null; + mFlags = other.mFlags; + mSideHint = other.mSideHint; + mBoundingRects = other.mBoundingRects != null ? other.mBoundingRects.clone() : null; + } + + public InsetsSource setFrame(int left, int top, int right, int bottom) { + mFrame.set(left, top, right, bottom); + return this; + } + + public InsetsSource setFrame(Rect frame) { + mFrame.set(frame); + return this; + } + + public InsetsSource setVisibleFrame(@Nullable Rect visibleFrame) { + mVisibleFrame = visibleFrame != null ? new Rect(visibleFrame) : null; + return this; + } + + public InsetsSource setVisible(boolean visible) { + mVisible = visible; + return this; + } + + public InsetsSource setFlags(@Flags int flags) { + mFlags = flags; + return this; + } + + public InsetsSource setFlags(@Flags int flags, @Flags int mask) { + mFlags = (mFlags & ~mask) | (flags & mask); + return this; + } + + /** + * Updates the side hint which is used to decide which side of the relative frame should receive + * insets when the frame fully covers the relative frame. + * + * @param bounds A rectangle which contains the frame. It will be used to calculate the hint. + */ + public InsetsSource updateSideHint(Rect bounds) { + mSideHint = getInsetSide(calculateInsets(bounds, mFrame, true /* ignoreVisibility */)); + return this; + } + + /** + * Set the bounding rectangles of this source. They are expected to be relative to the source + * frame. + */ + public InsetsSource setBoundingRects(@Nullable Rect[] rects) { + mBoundingRects = rects != null ? rects.clone() : null; + return this; + } + + public int getId() { + return mId; + } + + public @InsetsType int getType() { + return mType; + } + + public Rect getFrame() { + return mFrame; + } + + public @Nullable Rect getVisibleFrame() { + return mVisibleFrame; + } + + public boolean isVisible() { + return mVisible; + } + + public @Flags int getFlags() { + return mFlags; + } + + public boolean hasFlags(int flags) { + return (mFlags & flags) == flags; + } + + /** Returns the bounding rectangles of this source. */ + public @Nullable Rect[] getBoundingRects() { + return mBoundingRects; + } + + /** + * Calculates the insets this source will cause to a client window. + * + * @param relativeFrame The frame to calculate the insets relative to. + * @param ignoreVisibility If true, always reports back insets even if source isn't visible. + * @return The resulting insets. The contract is that only one side will be occupied by a + * source. + */ + public Insets calculateInsets(Rect relativeFrame, boolean ignoreVisibility) { + return calculateInsets(relativeFrame, mFrame, ignoreVisibility); + } + + /** Like {@link #calculateInsets(Rect, boolean)}, but will return visible insets. */ + public Insets calculateVisibleInsets(Rect relativeFrame) { + return calculateInsets( + relativeFrame, + mVisibleFrame != null ? mVisibleFrame : mFrame, + false /* ignoreVisibility */); + } + + private Insets calculateInsets(Rect relativeFrame, Rect frame, boolean ignoreVisibility) { + if (!ignoreVisibility && !mVisible) { + return Insets.NONE; + } + // During drag-move and drag-resizing, the caption insets position may not get updated + // before the app frame get updated. To layout the app content correctly during drag events, + // we always return the insets with the corresponding height covering the top. + // However, with the "fake" IME navigation bar treated as a caption bar, we return the + // insets with the corresponding height the bottom. + if (getType() == WindowInsets.Type.captionBar()) { + return getId() == ID_IME_CAPTION_BAR + ? Insets.of(0, 0, 0, frame.height()) + : Insets.of(0, frame.height(), 0, 0); + } + // Checks for whether there is shared edge with insets for 0-width/height window. + final boolean hasIntersection = + relativeFrame.isEmpty() + ? getIntersection(frame, relativeFrame, mTmpFrame) + : mTmpFrame.setIntersect(frame, relativeFrame); + if (!hasIntersection) { + return Insets.NONE; + } + + // TODO: Currently, non-floating IME always intersects at bottom due to issues with cutout. + // However, we should let the policy decide from the server. + if (getType() == WindowInsets.Type.ime()) { + return Insets.of(0, 0, 0, mTmpFrame.height()); + } + + if (mTmpFrame.equals(relativeFrame)) { + // Covering all sides + switch (mSideHint) { + default: + case SIDE_LEFT: + return Insets.of(mTmpFrame.width(), 0, 0, 0); + case SIDE_TOP: + return Insets.of(0, mTmpFrame.height(), 0, 0); + case SIDE_RIGHT: + return Insets.of(0, 0, mTmpFrame.width(), 0); + case SIDE_BOTTOM: + return Insets.of(0, 0, 0, mTmpFrame.height()); + } + } else if (mTmpFrame.width() == relativeFrame.width()) { + // Intersecting at top/bottom + if (mTmpFrame.top == relativeFrame.top) { + return Insets.of(0, mTmpFrame.height(), 0, 0); + } else if (mTmpFrame.bottom == relativeFrame.bottom) { + return Insets.of(0, 0, 0, mTmpFrame.height()); + } + // TODO: remove when insets are shell-customizable. + // This is a hack that says "if this is a top-inset (eg statusbar), always apply it + // to the top". It is used when adjusting primary split for IME. + if (mTmpFrame.top == 0) { + return Insets.of(0, mTmpFrame.height(), 0, 0); + } + } else if (mTmpFrame.height() == relativeFrame.height()) { + // Intersecting at left/right + if (mTmpFrame.left == relativeFrame.left) { + return Insets.of(mTmpFrame.width(), 0, 0, 0); + } else if (mTmpFrame.right == relativeFrame.right) { + return Insets.of(0, 0, mTmpFrame.width(), 0); + } + } + return Insets.NONE; + } + + /** Calculates the bounding rects the source will cause to a client window. */ + public @NonNull Rect[] calculateBoundingRects(Rect relativeFrame, boolean ignoreVisibility) { + if (!ignoreVisibility && !mVisible) { + return NO_BOUNDING_RECTS; + } + + final Rect frame = getFrame(); + if (mBoundingRects == null) { + // No bounding rects set, make a single bounding rect that covers the intersection of + // the |frame| and the |relativeFrame|. Also make it relative to the window origin. + return mTmpBoundingRect.setIntersect(frame, relativeFrame) + ? new Rect[] { + new Rect( + mTmpBoundingRect.left - relativeFrame.left, + mTmpBoundingRect.top - relativeFrame.top, + mTmpBoundingRect.right - relativeFrame.left, + mTmpBoundingRect.bottom - relativeFrame.top) + } + : NO_BOUNDING_RECTS; + } + + // Special treatment for captionBar inset type. During drag-resizing, the |frame| and + // |boundingRects| may not get updated as quickly as |relativeFrame|, so just assume the + // |frame| will always be either at the top or bottom of |relativeFrame|. This means some + // calculations to make |boundingRects| relative to |relativeFrame| can be skipped or + // simplified. + // TODO(b/254128050): remove special treatment. + if (getType() == WindowInsets.Type.captionBar()) { + final ArrayList validBoundingRects = new ArrayList<>(); + for (final Rect boundingRect : mBoundingRects) { + // Assume that the caption |frame| and |relativeFrame| perfectly align at the top + // or bottom, meaning that the provided |boundingRect|, which is relative to the + // |frame| either is already relative to |relativeFrame| (for top captionBar()), or + // just needs to be made relative to |relativeFrame| for bottom bars. + final int frameHeight = frame.height(); + mTmpBoundingRect.set(boundingRect); + if (getId() == ID_IME_CAPTION_BAR) { + mTmpBoundingRect.offset(0, relativeFrame.height() - frameHeight); + } + validBoundingRects.add(new Rect(mTmpBoundingRect)); + } + return validBoundingRects.toArray(new Rect[validBoundingRects.size()]); + } + + // Regular treatment for non-captionBar inset types. + final ArrayList validBoundingRects = new ArrayList<>(); + for (final Rect boundingRect : mBoundingRects) { + // |boundingRect| was provided relative to |frame|. Make it absolute to be in the same + // coordinate system as |frame|. + final Rect absBoundingRect = + new Rect( + boundingRect.left + frame.left, + boundingRect.top + frame.top, + boundingRect.right + frame.left, + boundingRect.bottom + frame.top); + // Now find the intersection of that |absBoundingRect| with |relativeFrame|. In other + // words, whichever part of the bounding rect is inside the window frame. + if (!mTmpBoundingRect.setIntersect(absBoundingRect, relativeFrame)) { + // It's possible for this to be empty if the frame and bounding rects were larger + // than the |relativeFrame|, such as when a system window is wider than the app + // window width. Just ignore that rect since it will have no effect on the + // window insets. + continue; + } + // At this point, |mTmpBoundingRect| is a valid bounding rect located fully inside the + // window, convert it to be relative to the window so that apps don't need to know the + // location of the window to understand bounding rects. + validBoundingRects.add( + new Rect( + mTmpBoundingRect.left - relativeFrame.left, + mTmpBoundingRect.top - relativeFrame.top, + mTmpBoundingRect.right - relativeFrame.left, + mTmpBoundingRect.bottom - relativeFrame.top)); + } + if (validBoundingRects.isEmpty()) { + return NO_BOUNDING_RECTS; + } + return validBoundingRects.toArray(new Rect[validBoundingRects.size()]); + } + + /** + * Outputs the intersection of two rectangles. The shared edges will also be counted in the + * intersection. + * + * @param a The first rectangle being intersected with. + * @param b The second rectangle being intersected with. + * @param out The rectangle which represents the intersection. + * @return {@code true} if there is any intersection. + */ + private static boolean getIntersection(@NonNull Rect a, @NonNull Rect b, @NonNull Rect out) { + if (a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom) { + out.left = Math.max(a.left, b.left); + out.top = Math.max(a.top, b.top); + out.right = Math.min(a.right, b.right); + out.bottom = Math.min(a.bottom, b.bottom); + return true; + } + out.setEmpty(); + return false; + } + + /** + * Retrieves the side for a certain {@code insets}. It is required that only one field l/t/r/b + * is set in order that this method returns a meaningful result. + */ + static @InternalInsetsSide int getInsetSide(Insets insets) { + if (Insets.NONE.equals(insets)) { + return SIDE_NONE; + } + if (insets.left != 0) { + return SIDE_LEFT; + } + if (insets.top != 0) { + return SIDE_TOP; + } + if (insets.right != 0) { + return SIDE_RIGHT; + } + if (insets.bottom != 0) { + return SIDE_BOTTOM; + } + return SIDE_UNKNOWN; + } + + static String sideToString(@InternalInsetsSide int side) { + switch (side) { + case SIDE_NONE: + return "NONE"; + case SIDE_LEFT: + return "LEFT"; + case SIDE_TOP: + return "TOP"; + case SIDE_RIGHT: + return "RIGHT"; + case SIDE_BOTTOM: + return "BOTTOM"; + default: + return "UNKNOWN:" + side; + } + } + + /** + * Creates an identifier of an {@link InsetsSource}. + * + * @param owner An object owned by the owner. Only the owner can modify its own sources. + * @param index An owner may have multiple sources with the same type. For example, the system + * server might have multiple display cutout sources. This is used to identify which one is + * which. The value must be in a range of [0, 2047]. + * @param type The {@link InsetsType type} of the source. + * @return a unique integer as the identifier. + */ + public static int createId( + Object owner, @IntRange(from = 0, to = 2047) int index, @InsetsType int type) { + if (index < 0 || index >= 2048) { + throw new IllegalArgumentException(); + } + // owner takes top 16 bits; + // index takes 11 bits since the 6th bit; + // type takes bottom 5 bits. + return ((System.identityHashCode(owner) % (1 << 16)) << 16) + + (index << 5) + + WindowInsets.Type.indexOf(type); + } + + /** + * Gets the index from the ID. + * + * @see #createId(Object, int, int) + */ + public static int getIndex(int id) { + // start: ????????????????***********????? + // & 65535: 0000000000000000***********????? + // >> 5: 000000000000000000000*********** + return (id & 65535) >> 5; + } + + /** + * Gets the {@link InsetsType} from the ID. + * + * @see #createId(Object, int, int) + * @see WindowInsets.Type#indexOf(int) + */ + public static int getType(int id) { + // start: ???????????????????????????***** + // & 31: 000000000000000000000000000***** + // 1 <<: See WindowInsets.Type#indexOf + return 1 << (id & 31); + } + + public static String flagsToString(@Flags int flags) { + final StringJoiner joiner = new StringJoiner("|"); + if ((flags & FLAG_SUPPRESS_SCRIM) != 0) { + joiner.add("SUPPRESS_SCRIM"); + } + if ((flags & FLAG_INSETS_ROUNDED_CORNER) != 0) { + joiner.add("INSETS_ROUNDED_CORNER"); + } + if ((flags & FLAG_FORCE_CONSUMING) != 0) { + joiner.add("FORCE_CONSUMING"); + } + if ((flags & FLAG_ANIMATE_RESIZING) != 0) { + joiner.add("ANIMATE_RESIZING"); + } + return joiner.toString(); + } + + /** + * Export the state of {@link InsetsSource} into a protocol buffer output stream. + * + * @param proto Stream to write the state to + * @param fieldId FieldId of InsetsSource as defined in the parent message + */ + public void dumpDebug(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + if (!android.os.Flags.androidOsBuildVanillaIceCream()) { + // Deprecated since V. + proto.write(TYPE, WindowInsets.Type.toString(mType)); + } + mFrame.dumpDebug(proto, FRAME); + if (mVisibleFrame != null) { + mVisibleFrame.dumpDebug(proto, VISIBLE_FRAME); + } + proto.write(VISIBLE, mVisible); + proto.write(TYPE_NUMBER, mType); + proto.end(token); + } + + public void dump(String prefix, PrintWriter pw) { + pw.print(prefix); + pw.print("InsetsSource id="); + pw.print(Integer.toHexString(mId)); + pw.print(" type="); + pw.print(WindowInsets.Type.toString(mType)); + pw.print(" frame="); + pw.print(mFrame.toShortString()); + if (mVisibleFrame != null) { + pw.print(" visibleFrame="); + pw.print(mVisibleFrame.toShortString()); + } + pw.print(" visible="); + pw.print(mVisible); + pw.print(" flags="); + pw.print(flagsToString(mFlags)); + pw.print(" sideHint="); + pw.print(sideToString(mSideHint)); + pw.print(" boundingRects="); + pw.print(Arrays.toString(mBoundingRects)); + pw.println(); + } + + @Override + public boolean equals(@Nullable Object o) { + return equals(o, false); + } + + /** + * @param excludeInvisibleImeFrames If {@link WindowInsets.Type#ime()} frames should be ignored + * when IME is not visible. + */ + public boolean equals(@Nullable Object o, boolean excludeInvisibleImeFrames) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + InsetsSource that = (InsetsSource) o; + + if (mId != that.mId) return false; + if (mType != that.mType) return false; + if (mVisible != that.mVisible) return false; + if (mFlags != that.mFlags) return false; + if (mSideHint != that.mSideHint) return false; + if (excludeInvisibleImeFrames && !mVisible && mType == WindowInsets.Type.ime()) return true; + if (!Objects.equals(mVisibleFrame, that.mVisibleFrame)) return false; + if (!mFrame.equals(that.mFrame)) return false; + return Arrays.equals(mBoundingRects, that.mBoundingRects); + } + + @Override + public int hashCode() { + return Objects.hash( + mId, + mType, + mFrame, + mVisibleFrame, + mVisible, + mFlags, + mSideHint, + Arrays.hashCode(mBoundingRects)); + } + + public InsetsSource(Parcel in) { + mId = in.readInt(); + mType = in.readInt(); + mFrame = Rect.CREATOR.createFromParcel(in); + if (in.readInt() != 0) { + mVisibleFrame = Rect.CREATOR.createFromParcel(in); + } else { + mVisibleFrame = null; + } + mVisible = in.readBoolean(); + mFlags = in.readInt(); + mSideHint = in.readInt(); + mBoundingRects = in.createTypedArray(Rect.CREATOR); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mId); + dest.writeInt(mType); + mFrame.writeToParcel(dest, 0); + if (mVisibleFrame != null) { + dest.writeInt(1); + mVisibleFrame.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + dest.writeBoolean(mVisible); + dest.writeInt(mFlags); + dest.writeInt(mSideHint); + dest.writeTypedArray(mBoundingRects, flags); + } + + @Override + public String toString() { + return "InsetsSource: {" + + Integer.toHexString(mId) + + " mType=" + + WindowInsets.Type.toString(mType) + + " mFrame=" + + mFrame.toShortString() + + " mVisible=" + + mVisible + + " mFlags=" + + flagsToString(mFlags) + + " mSideHint=" + + sideToString(mSideHint) + + " mBoundingRects=" + + Arrays.toString(mBoundingRects) + + "}"; + } + + public static final @NonNull Creator CREATOR = + new Creator<>() { + + public InsetsSource createFromParcel(Parcel in) { + return new InsetsSource(in); + } + + public InsetsSource[] newArray(int size) { + return new InsetsSource[size]; + } + }; +} diff --git a/compatLib/src/main/java/android/window/OnBackAnimationCallback.java b/compatLib/src/main/java/android/window/OnBackAnimationCallback.java new file mode 100644 index 0000000000..b2e5624baa --- /dev/null +++ b/compatLib/src/main/java/android/window/OnBackAnimationCallback.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 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 android.window; + +import android.annotation.NonNull; +import android.app.Activity; +import android.app.Dialog; +import android.view.View; +import android.view.Window; + +/** + * Interface for applications to register back animation callbacks along their custom back handling. + * + *

This allows the client to customize various back behaviors by overriding the corresponding + * callback methods. + * + *

Callback instances can be added to and removed from {@link OnBackInvokedDispatcher}, which is + * held at window level and accessible through {@link Activity#getOnBackInvokedDispatcher()}, {@link + * Dialog#getOnBackInvokedDispatcher()}, {@link Window#getOnBackInvokedDispatcher()} and {@link + * View#findOnBackInvokedDispatcher()}. + * + *

When back is triggered, callbacks on the in-focus window are invoked in reverse order in which + * they are added within the same priority. Between different priorities, callbacks with higher + * priority are invoked first. + * + *

+ * + * @see OnBackInvokedCallback + */ +public interface OnBackAnimationCallback extends OnBackInvokedCallback { + /** + * Called when a back gesture has been started, or back button has been pressed down. + * + * @param backEvent The {@link BackEvent} containing information about the touch or button + * press. + * @see BackEvent + */ + default void onBackStarted(@NonNull BackEvent backEvent) {} + + /** + * Called when a back gesture progresses. + * + * @param backEvent An {@link BackEvent} object describing the progress event. + * @see BackEvent + */ + default void onBackProgressed(@NonNull BackEvent backEvent) {} + + /** Called when a back gesture or back button press has been cancelled. */ + default void onBackCancelled() {} +} diff --git a/compatLib/src/main/java/android/window/OnBackInvokedCallback.java b/compatLib/src/main/java/android/window/OnBackInvokedCallback.java new file mode 100644 index 0000000000..ab6b08971e --- /dev/null +++ b/compatLib/src/main/java/android/window/OnBackInvokedCallback.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2022 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 android.window; + +import android.app.Activity; +import android.app.Dialog; +import android.view.View; +import android.view.Window; + +/** + * Callback allowing applications to handle back events in place of the system. + * + *

Callback instances can be added to and removed from {@link OnBackInvokedDispatcher}, which is + * held at window level and accessible through {@link Activity#getOnBackInvokedDispatcher()}, {@link + * Dialog#getOnBackInvokedDispatcher()}, {@link Window#getOnBackInvokedDispatcher()} and {@link + * View#findOnBackInvokedDispatcher()}. + * + *

When back is triggered, callbacks on the in-focus window are invoked in reverse order in which + * they are added within the same priority. Between different priorities, callbacks with higher + * priority are invoked first. + * + *

This replaces {@link Activity#onBackPressed()}, {@link Dialog#onBackPressed()} and {@link + * android.view.KeyEvent#KEYCODE_BACK} + * + *

If you want to customize back animation behaviors, in addition to handling back invocations, + * register its subclass instances {@link OnBackAnimationCallback} instead. + * + *

+ * + * @see OnBackInvokedDispatcher#registerOnBackInvokedCallback(int, OnBackInvokedCallback) + * registerOnBackInvokedCallback(priority, OnBackInvokedCallback) to specify callback priority. + */ +@SuppressWarnings("deprecation") +public interface OnBackInvokedCallback { + /** + * Called when a back gesture has been completed and committed, or back button pressed has been + * released and committed. + */ + void onBackInvoked(); +} diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java index cf21b3d619..2da9c3e89f 100644 --- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java +++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java @@ -32,14 +32,19 @@ import android.view.ViewTreeObserver; import com.android.launcher3.BaseActivity; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; +import com.android.launcher3.Utilities; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.statemanager.StateManager.StateHandler; import com.android.launcher3.states.StateAnimationConfig; import com.android.quickstep.util.BaseDepthController; +import com.patrykmichalik.opto.core.PreferenceExtensionsKt; import java.io.PrintWriter; import java.util.function.Consumer; +import app.lawnchair.compat.LawnchairQuickstepCompat; +import app.lawnchair.preferences2.PreferenceManager2; + /** * Controls blur and wallpaper zoom, for the Launcher surface only. */ @@ -57,14 +62,24 @@ public class DepthController extends BaseDepthController implements StateHandler private View.OnAttachStateChangeListener mOnAttachListener; + private final boolean mEnableDepth; + public DepthController(Launcher l) { super(l); + var pref = PreferenceManager2.getInstance(l).getWallpaperDepthEffect(); + mEnableDepth = PreferenceExtensionsKt.firstBlocking(pref); } private void onLauncherDraw() { View view = mLauncher.getDragLayer(); ViewRootImpl viewRootImpl = view.getViewRootImpl(); - setSurface(viewRootImpl != null ? viewRootImpl.getSurfaceControl() : null); + try { + if (Utilities.ATLEAST_Q) { + setSurface(viewRootImpl != null ? viewRootImpl.getSurfaceControl() : null); + } + } catch (Throwable t) { + // Ignore any exceptions + } view.post(() -> view.getViewTreeObserver().removeOnDrawListener(mOnDrawListener)); } @@ -74,8 +89,12 @@ public class DepthController extends BaseDepthController implements StateHandler mOnAttachListener = new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View view) { - CrossWindowBlurListeners.getInstance().addListener(mLauncher.getMainExecutor(), - mCrossWindowBlurListener); + try { + CrossWindowBlurListeners.getInstance().addListener(mLauncher.getMainExecutor(), + mCrossWindowBlurListener); + } catch (Throwable t) { + // Ignore + } mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener); // To handle the case where window token is invalid during last setDepth call. @@ -108,7 +127,11 @@ public class DepthController extends BaseDepthController implements StateHandler private void removeSecondaryListeners() { if (mCrossWindowBlurListener != null) { - CrossWindowBlurListeners.getInstance().removeListener(mCrossWindowBlurListener); + try { + CrossWindowBlurListeners.getInstance().removeListener(mCrossWindowBlurListener); + } catch (Throwable t) { + // Ignore + } } if (mOpaquenessListener != null) { mLauncher.getScrimView().removeOpaquenessListener(mOpaquenessListener); @@ -154,8 +177,14 @@ public class DepthController extends BaseDepthController implements StateHandler @Override protected void applyDepthAndBlur() { - ensureDependencies(); - super.applyDepthAndBlur(); + try { + if (LawnchairQuickstepCompat.ATLEAST_R && mEnableDepth) { + ensureDependencies(); + super.applyDepthAndBlur(); + } + } catch (Throwable t) { + // Ignore + } } @Override diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index 53a311a303..3d8426fb34 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -185,30 +185,34 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle, SafeCloseable { * different process). It is bare-bones, so it's expected that the component and options will * be provided via fill-in intent. */ - private final PendingIntent mRecentsPendingIntent; + private PendingIntent mRecentsPendingIntent; @Nullable - private final ProxyUnfoldTransitionProvider mUnfoldTransitionProvider; + private ProxyUnfoldTransitionProvider mUnfoldTransitionProvider; private SystemUiProxy(Context context) { mContext = context; mAsyncHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessageAsync); final Intent baseIntent = new Intent().setPackage(mContext.getPackageName()); - final ActivityOptions options = ActivityOptions.makeBasic(); - if (Utilities.ATLEAST_U) { - options.setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + if (Utilities.ATLEAST_Q) { + + final ActivityOptions options = ActivityOptions.makeBasic(); + if (Utilities.ATLEAST_U) { + options.setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + } + + mRecentsPendingIntent = LawnchairQuickstepCompat.ATLEAST_V ? PendingIntent.getActivity(mContext, 0, baseIntent, + PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT + | Intent.FILL_IN_COMPONENT, options.toBundle()) : PendingIntent.getActivity(mContext, 0, baseIntent, + PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT + | Intent.FILL_IN_COMPONENT) ; + + mUnfoldTransitionProvider = + (enableUnfoldStateAnimation() && new ResourceUnfoldTransitionConfig().isEnabled()) + ? new ProxyUnfoldTransitionProvider() : null; } - mRecentsPendingIntent = LawnchairQuickstepCompat.ATLEAST_V ? PendingIntent.getActivity(mContext, 0, baseIntent, - PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT - | Intent.FILL_IN_COMPONENT) : PendingIntent.getActivity(mContext, 0, baseIntent, - PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT - | Intent.FILL_IN_COMPONENT, options.toBundle()) ; - - mUnfoldTransitionProvider = - (enableUnfoldStateAnimation() && new ResourceUnfoldTransitionConfig().isEnabled()) - ? new ProxyUnfoldTransitionProvider() : null; } @Override diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java index 53932849fc..acd2bfa416 100644 --- a/src/com/android/launcher3/pm/InstallSessionHelper.java +++ b/src/com/android/launcher3/pm/InstallSessionHelper.java @@ -228,7 +228,7 @@ public class InstallSessionHelper implements SafeCloseable { && !promiseIconAddedForId(sessionInfo.getSessionId())) { // In case of unarchival, we do not want to add a workspace promise icon if one is // not already present. For general app installations however, we do support it. - if (!Flags.enableSupportForArchiving() || !sessionInfo.isUnarchival()) { + if (ATLEAST_V && (!Flags.enableSupportForArchiving() || !sessionInfo.isUnarchival())) { FileLog.d(LOG, "Adding package name to install queue: " + sessionInfo.getAppPackageName()); diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java index 24d58f3e98..1e00ca8b05 100644 --- a/src/com/android/launcher3/pm/InstallSessionTracker.java +++ b/src/com/android/launcher3/pm/InstallSessionTracker.java @@ -81,7 +81,7 @@ public class InstallSessionTracker extends PackageInstaller.SessionCallback { helper.tryQueuePromiseAppIcon(sessionInfo); - if (Flags.enableSupportForArchiving() && sessionInfo != null + if (Utilities.ATLEAST_V && Flags.enableSupportForArchiving() && sessionInfo != null && sessionInfo.isUnarchival()) { // For archived apps, icon could already be present on the workspace. To make sure // the icon state is updated, we send a change event. diff --git a/src/com/android/launcher3/util/BackPressHandler.java b/src/com/android/launcher3/util/BackPressHandler.java index b63f648b2d..fbd4225d64 100644 --- a/src/com/android/launcher3/util/BackPressHandler.java +++ b/src/com/android/launcher3/util/BackPressHandler.java @@ -15,16 +15,12 @@ */ package com.android.launcher3.util; -import android.os.Build; import android.window.OnBackAnimationCallback; -import androidx.annotation.RequiresApi; - /** * Extension of {@link OnBackAnimationCallback} that allows a check to determine * if this callback supports handling back or not */ -@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public interface BackPressHandler extends OnBackAnimationCallback { boolean canHandleBack(); }