diff options
Diffstat (limited to 'libs')
213 files changed, 8212 insertions, 4305 deletions
diff --git a/libs/WindowManager/Jetpack/Android.bp b/libs/WindowManager/Jetpack/Android.bp index 7fbbb61e469d..4612ba28d1db 100644 --- a/libs/WindowManager/Jetpack/Android.bp +++ b/libs/WindowManager/Jetpack/Android.bp @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Sidecar android_library_import { name: "window-sidecar", aars: ["window-sidecar-release.aar"], @@ -20,7 +21,7 @@ android_library_import { java_library { name: "androidx.window.sidecar", - srcs: ["src/**/*.java"], + srcs: ["src/androidx/window/sidecar/**/*.java", "src/androidx/window/util/**/*.java"], static_libs: ["window-sidecar"], installable: true, sdk_version: "core_platform", @@ -36,3 +37,31 @@ prebuilt_etc { src: "androidx.window.sidecar.xml", filename_from_src: true, } + +// Extensions +// NOTE: This module is still under active development and must not +// be used in production. Use 'androidx.window.sidecar' instead. +android_library_import { + name: "window-extensions", + aars: ["window-extensions-release.aar"], + sdk_version: "current", +} + +java_library { + name: "androidx.window.extensions", + srcs: ["src/androidx/window/extensions/**/*.java", "src/androidx/window/util/**/*.java"], + static_libs: ["window-extensions"], + installable: true, + sdk_version: "core_platform", + system_ext_specific: true, + libs: ["framework", "androidx.annotation_annotation",], + required: ["androidx.window.extensions.xml",], +} + +prebuilt_etc { + name: "androidx.window.extensions.xml", + system_ext_specific: true, + sub_dir: "permissions", + src: "androidx.window.extensions.xml", + filename_from_src: true, +} diff --git a/libs/WindowManager/Jetpack/androidx.window.extensions.xml b/libs/WindowManager/Jetpack/androidx.window.extensions.xml new file mode 100644 index 000000000000..34264aa3ade3 --- /dev/null +++ b/libs/WindowManager/Jetpack/androidx.window.extensions.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<permissions> + <library + name="androidx.window.extensions" + file="/system_ext/framework/androidx.window.extensions.jar"/> +</permissions> diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java new file mode 100644 index 000000000000..b7a60392c512 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2021 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 androidx.window.extensions; + +import android.content.Context; + +/** + * Provider class that will instantiate the library implementation. It must be included in the + * vendor library, and the vendor implementation must match the signature of this class. + */ +public class ExtensionProvider { + /** + * Provides a simple implementation of {@link ExtensionInterface} that can be replaced by + * an OEM by overriding this method. + */ + public static ExtensionInterface getExtensionImpl(Context context) { + return new SampleExtensionImpl(context); + } + + /** + * The support library will use this method to check API version compatibility. + * @return API version string in MAJOR.MINOR.PATCH-description format. + */ + public static String getApiVersion() { + return "1.0.0-settings_sample"; + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java new file mode 100644 index 000000000000..5c91cf41bfc6 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2021 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 androidx.window.extensions; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation; +import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Rect; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.window.util.BaseDisplayFeature; +import androidx.window.util.SettingsConfigProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + * Reference implementation of androidx.window.extensions OEM interface for use with + * WindowManager Jetpack. + * + * NOTE: This version is a work in progress and under active development. It MUST NOT be used in + * production builds since the interface can still change before reaching stable version. + * Please refer to {@link androidx.window.sidecar.SampleSidecarImpl} instead. + */ +class SampleExtensionImpl extends StubExtension implements + SettingsConfigProvider.StateChangeCallback { + private static final String TAG = "SampleExtension"; + + private final SettingsConfigProvider mConfigProvider; + + SampleExtensionImpl(Context context) { + mConfigProvider = new SettingsConfigProvider(context, this); + } + + @Override + public void onDevicePostureChanged() { + updateDeviceState(new ExtensionDeviceState(mConfigProvider.getDeviceState())); + } + + @Override + public void onDisplayFeaturesChanged() { + for (Activity activity : getActivitiesListeningForLayoutChanges()) { + ExtensionWindowLayoutInfo newLayout = getWindowLayoutInfo(activity); + updateWindowLayout(activity, newLayout); + } + } + + @NonNull + private ExtensionWindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) { + List<ExtensionDisplayFeature> displayFeatures = getDisplayFeatures(activity); + return new ExtensionWindowLayoutInfo(displayFeatures); + } + + private List<ExtensionDisplayFeature> getDisplayFeatures(@NonNull Activity activity) { + List<ExtensionDisplayFeature> features = new ArrayList<>(); + int displayId = activity.getDisplay().getDisplayId(); + if (displayId != DEFAULT_DISPLAY) { + Log.w(TAG, "This sample doesn't support display features on secondary displays"); + return features; + } + + if (activity.isInMultiWindowMode()) { + // It is recommended not to report any display features in multi-window mode, since it + // won't be possible to synchronize the display feature positions with window movement. + return features; + } + + List<BaseDisplayFeature> storedFeatures = mConfigProvider.getDisplayFeatures(); + for (BaseDisplayFeature baseFeature : storedFeatures) { + Rect featureRect = baseFeature.getRect(); + rotateRectToDisplayRotation(displayId, featureRect); + transformToWindowSpaceRect(activity, featureRect); + features.add(new ExtensionFoldingFeature(featureRect, baseFeature.getType(), + baseFeature.getState())); + } + return features; + } + + @Override + protected void onListenersChanged() { + if (hasListeners()) { + mConfigProvider.registerObserversIfNeeded(); + } else { + mConfigProvider.unregisterObserversIfNeeded(); + } + + onDevicePostureChanged(); + onDisplayFeaturesChanged(); + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java new file mode 100644 index 000000000000..b0895efc75a9 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2021 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 androidx.window.extensions; + +import android.app.Activity; + +import androidx.annotation.NonNull; + +import java.util.HashSet; +import java.util.Set; + +/** + * Basic implementation of the {@link ExtensionInterface}. An OEM can choose to use it as the base + * class for their implementation. + */ +abstract class StubExtension implements ExtensionInterface { + + private ExtensionCallback mExtensionCallback; + private final Set<Activity> mWindowLayoutChangeListenerActivities = new HashSet<>(); + private boolean mDeviceStateChangeListenerRegistered; + + StubExtension() { + } + + @Override + public void setExtensionCallback(@NonNull ExtensionCallback extensionCallback) { + this.mExtensionCallback = extensionCallback; + } + + @Override + public void onWindowLayoutChangeListenerAdded(@NonNull Activity activity) { + this.mWindowLayoutChangeListenerActivities.add(activity); + this.onListenersChanged(); + } + + @Override + public void onWindowLayoutChangeListenerRemoved(@NonNull Activity activity) { + this.mWindowLayoutChangeListenerActivities.remove(activity); + this.onListenersChanged(); + } + + @Override + public void onDeviceStateListenersChanged(boolean isEmpty) { + this.mDeviceStateChangeListenerRegistered = !isEmpty; + this.onListenersChanged(); + } + + void updateDeviceState(ExtensionDeviceState newState) { + if (this.mExtensionCallback != null) { + mExtensionCallback.onDeviceStateChanged(newState); + } + } + + void updateWindowLayout(@NonNull Activity activity, + @NonNull ExtensionWindowLayoutInfo newLayout) { + if (this.mExtensionCallback != null) { + mExtensionCallback.onWindowLayoutChanged(activity, newLayout); + } + } + + @NonNull + Set<Activity> getActivitiesListeningForLayoutChanges() { + return mWindowLayoutChangeListenerActivities; + } + + protected boolean hasListeners() { + return !mWindowLayoutChangeListenerActivities.isEmpty() + || mDeviceStateChangeListenerRegistered; + } + + protected abstract void onListenersChanged(); +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java new file mode 100644 index 000000000000..d3700f88d97f --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2020 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 androidx.window.sidecar; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation; +import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; + +import android.app.Activity; +import android.app.ActivityThread; +import android.content.Context; +import android.graphics.Rect; +import android.os.IBinder; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.window.util.BaseDisplayFeature; +import androidx.window.util.SettingsConfigProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + * Reference implementation of androidx.window.sidecar OEM interface for use with + * WindowManager Jetpack. + */ +class SampleSidecarImpl extends StubSidecar implements + SettingsConfigProvider.StateChangeCallback { + private static final String TAG = "SampleSidecar"; + + private final SettingsConfigProvider mConfigProvider; + + SampleSidecarImpl(Context context) { + mConfigProvider = new SettingsConfigProvider(context, this); + } + + @Override + public void onDevicePostureChanged() { + updateDeviceState(getDeviceState()); + } + + @Override + public void onDisplayFeaturesChanged() { + for (IBinder windowToken : getWindowsListeningForLayoutChanges()) { + SidecarWindowLayoutInfo newLayout = getWindowLayoutInfo(windowToken); + updateWindowLayout(windowToken, newLayout); + } + } + + @NonNull + @Override + public SidecarDeviceState getDeviceState() { + SidecarDeviceState deviceState = new SidecarDeviceState(); + deviceState.posture = mConfigProvider.getDeviceState(); + return deviceState; + } + + @NonNull + @Override + public SidecarWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) { + Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken); + SidecarWindowLayoutInfo windowLayoutInfo = new SidecarWindowLayoutInfo(); + if (activity == null) { + return windowLayoutInfo; + } + windowLayoutInfo.displayFeatures = getDisplayFeatures(activity); + return windowLayoutInfo; + } + + private List<SidecarDisplayFeature> getDisplayFeatures(@NonNull Activity activity) { + List<SidecarDisplayFeature> features = new ArrayList<SidecarDisplayFeature>(); + int displayId = activity.getDisplay().getDisplayId(); + if (displayId != DEFAULT_DISPLAY) { + Log.w(TAG, "This sample doesn't support display features on secondary displays"); + return features; + } + + if (activity.isInMultiWindowMode()) { + // It is recommended not to report any display features in multi-window mode, since it + // won't be possible to synchronize the display feature positions with window movement. + return features; + } + + List<BaseDisplayFeature> storedFeatures = mConfigProvider.getDisplayFeatures(); + for (BaseDisplayFeature baseFeature : storedFeatures) { + SidecarDisplayFeature feature = new SidecarDisplayFeature(); + Rect featureRect = baseFeature.getRect(); + rotateRectToDisplayRotation(displayId, featureRect); + transformToWindowSpaceRect(activity, featureRect); + feature.setRect(featureRect); + feature.setType(baseFeature.getType()); + features.add(feature); + } + return features; + } + + @Override + protected void onListenersChanged() { + if (hasListeners()) { + mConfigProvider.registerObserversIfNeeded(); + } else { + mConfigProvider.unregisterObserversIfNeeded(); + } + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java deleted file mode 100644 index 5397302f6882..000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright (C) 2020 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 androidx.window.sidecar; - -import static android.view.Display.DEFAULT_DISPLAY; - -import static androidx.window.sidecar.SidecarHelper.getWindowDisplay; -import static androidx.window.sidecar.SidecarHelper.isInMultiWindow; -import static androidx.window.sidecar.SidecarHelper.rotateRectToDisplayRotation; -import static androidx.window.sidecar.SidecarHelper.transformToWindowSpaceRect; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.ContentObserver; -import android.graphics.Rect; -import android.net.Uri; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.provider.Settings; -import android.text.TextUtils; -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.android.internal.R; - -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -class SettingsSidecarImpl extends StubSidecar { - private static final String TAG = "SettingsSidecar"; - - private static final String DEVICE_POSTURE = "device_posture"; - private static final String DISPLAY_FEATURES = "display_features"; - - private static final Pattern FEATURE_PATTERN = - Pattern.compile("([a-z]+)-\\[(\\d+),(\\d+),(\\d+),(\\d+)]"); - - private static final String FEATURE_TYPE_FOLD = "fold"; - private static final String FEATURE_TYPE_HINGE = "hinge"; - - private Context mContext; - private SettingsObserver mSettingsObserver; - - final class SettingsObserver extends ContentObserver { - private final Uri mDevicePostureUri = - Settings.Global.getUriFor(DEVICE_POSTURE); - private final Uri mDisplayFeaturesUri = - Settings.Global.getUriFor(DISPLAY_FEATURES); - private final ContentResolver mResolver = mContext.getContentResolver(); - private boolean mRegisteredObservers; - - - private SettingsObserver() { - super(new Handler(Looper.getMainLooper())); - } - - private void registerObserversIfNeeded() { - if (mRegisteredObservers) { - return; - } - mRegisteredObservers = true; - mResolver.registerContentObserver(mDevicePostureUri, false /* notifyForDescendents */, - this /* ContentObserver */); - mResolver.registerContentObserver(mDisplayFeaturesUri, false /* notifyForDescendents */, - this /* ContentObserver */); - } - - private void unregisterObserversIfNeeded() { - if (!mRegisteredObservers) { - return; - } - mRegisteredObservers = false; - mResolver.unregisterContentObserver(this); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - if (uri == null) { - return; - } - - if (mDevicePostureUri.equals(uri)) { - updateDevicePosture(); - return; - } - if (mDisplayFeaturesUri.equals(uri)) { - updateDisplayFeatures(); - return; - } - } - } - - SettingsSidecarImpl(Context context) { - mContext = context; - mSettingsObserver = new SettingsObserver(); - } - - private void updateDevicePosture() { - updateDeviceState(getDeviceState()); - } - - /** Update display features with values read from settings. */ - private void updateDisplayFeatures() { - for (IBinder windowToken : getWindowsListeningForLayoutChanges()) { - SidecarWindowLayoutInfo newLayout = getWindowLayoutInfo(windowToken); - updateWindowLayout(windowToken, newLayout); - } - } - - @NonNull - @Override - public SidecarDeviceState getDeviceState() { - ContentResolver resolver = mContext.getContentResolver(); - int posture = Settings.Global.getInt(resolver, DEVICE_POSTURE, - SidecarDeviceState.POSTURE_UNKNOWN); - SidecarDeviceState deviceState = new SidecarDeviceState(); - deviceState.posture = posture; - return deviceState; - } - - @NonNull - @Override - public SidecarWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) { - List<SidecarDisplayFeature> displayFeatures = readDisplayFeatures(windowToken); - SidecarWindowLayoutInfo windowLayoutInfo = new SidecarWindowLayoutInfo(); - windowLayoutInfo.displayFeatures = displayFeatures; - return windowLayoutInfo; - } - - private List<SidecarDisplayFeature> readDisplayFeatures(IBinder windowToken) { - List<SidecarDisplayFeature> features = new ArrayList<SidecarDisplayFeature>(); - int displayId = getWindowDisplay(windowToken); - if (displayId != DEFAULT_DISPLAY) { - Log.w(TAG, "This sample doesn't support display features on secondary displays"); - return features; - } - - if (isInMultiWindow(windowToken)) { - // It is recommended not to report any display features in multi-window mode, since it - // won't be possible to synchronize the display feature positions with window movement. - return features; - } - - ContentResolver resolver = mContext.getContentResolver(); - String displayFeaturesString = Settings.Global.getString(resolver, DISPLAY_FEATURES); - if (TextUtils.isEmpty(displayFeaturesString)) { - displayFeaturesString = mContext.getResources().getString( - R.string.config_display_features); - } - if (TextUtils.isEmpty(displayFeaturesString)) { - return features; - } - - String[] featureStrings = displayFeaturesString.split(";"); - for (String featureString : featureStrings) { - Matcher featureMatcher = FEATURE_PATTERN.matcher(featureString); - if (!featureMatcher.matches()) { - Log.e(TAG, "Malformed feature description format: " + featureString); - continue; - } - try { - String featureType = featureMatcher.group(1); - int type; - switch (featureType) { - case FEATURE_TYPE_FOLD: - type = SidecarDisplayFeature.TYPE_FOLD; - break; - case FEATURE_TYPE_HINGE: - type = SidecarDisplayFeature.TYPE_HINGE; - break; - default: { - Log.e(TAG, "Malformed feature type: " + featureType); - continue; - } - } - - int left = Integer.parseInt(featureMatcher.group(2)); - int top = Integer.parseInt(featureMatcher.group(3)); - int right = Integer.parseInt(featureMatcher.group(4)); - int bottom = Integer.parseInt(featureMatcher.group(5)); - Rect featureRect = new Rect(left, top, right, bottom); - rotateRectToDisplayRotation(featureRect, displayId); - transformToWindowSpaceRect(featureRect, windowToken); - if (isNotZero(featureRect)) { - SidecarDisplayFeature feature = new SidecarDisplayFeature(); - feature.setRect(featureRect); - feature.setType(type); - features.add(feature); - } else { - Log.w(TAG, "Failed to adjust feature to window"); - } - } catch (NumberFormatException e) { - Log.e(TAG, "Malformed feature description: " + featureString); - } - } - return features; - } - - private static boolean isNotZero(Rect rect) { - return rect.height() > 0 || rect.width() > 0; - } - - @Override - protected void onListenersChanged() { - if (mSettingsObserver == null) { - return; - } - - if (hasListeners()) { - mSettingsObserver.registerObserversIfNeeded(); - } else { - mSettingsObserver.unregisterObserversIfNeeded(); - } - } -} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java index 0b4915ed5dac..e6f8388b031f 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java @@ -28,7 +28,7 @@ public class SidecarProvider { * an OEM by overriding this method. */ public static SidecarInterface getSidecarImpl(Context context) { - return new SettingsSidecarImpl(context); + return new SampleSidecarImpl(context); } /** diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDisplayFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDisplayFeature.java new file mode 100644 index 000000000000..b74a2a4bd47d --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDisplayFeature.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2021 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 androidx.window.util; + +import android.graphics.Rect; + +import androidx.annotation.NonNull; + +/** Wrapper for both Extension and Sidecar versions of DisplayFeature. */ +public class BaseDisplayFeature { + private final int mType; + private final int mState; + @NonNull + public final Rect mRect; + + public BaseDisplayFeature(int type, int state, @NonNull Rect rect) { + this.mType = type; + this.mState = state; + if (rect.width() == 0 && rect.height() == 0) { + throw new IllegalArgumentException( + "Display feature rectangle cannot have zero width and height simultaneously."); + } + this.mRect = rect; + } + + public int getType() { + return mType; + } + + public int getState() { + return mState; + } + + @NonNull + public Rect getRect() { + return mRect; + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java index e5b6cff17b26..2a593f15a9de 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2021 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. @@ -14,30 +14,36 @@ * limitations under the License. */ -package androidx.window.sidecar; +package androidx.window.util; -import static android.view.Display.INVALID_DISPLAY; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; import android.app.Activity; -import android.app.ActivityThread; import android.graphics.Rect; import android.hardware.display.DisplayManagerGlobal; -import android.os.IBinder; import android.view.DisplayInfo; import android.view.Surface; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; -class SidecarHelper { +/** + * Util class for both Sidecar and Extensions. + */ +public final class ExtensionHelper { + + private ExtensionHelper() { + // Util class, no instances should be created. + } + /** - * Rotate the input rectangle specified in default display orientation to the current display + * Rotates the input rectangle specified in default display orientation to the current display * rotation. */ - static void rotateRectToDisplayRotation(Rect inOutRect, int displayId) { + public static void rotateRectToDisplayRotation(int displayId, Rect inOutRect) { DisplayManagerGlobal dmGlobal = DisplayManagerGlobal.getInstance(); DisplayInfo displayInfo = dmGlobal.getDisplayInfo(displayId); int rotation = displayInfo.rotation; @@ -52,7 +58,7 @@ class SidecarHelper { } /** - * Rotate the input rectangle within parent bounds for a given delta. + * Rotates the input rectangle within parent bounds for a given delta. */ private static void rotateBounds(Rect inOutRect, int parentWidth, int parentHeight, @Surface.Rotation int delta) { @@ -79,9 +85,9 @@ class SidecarHelper { } } - /** Transform rectangle from absolute coordinate space to the window coordinate space. */ - static void transformToWindowSpaceRect(Rect inOutRect, IBinder windowToken) { - Rect windowRect = getWindowBounds(windowToken); + /** Transforms rectangle from absolute coordinate space to the window coordinate space. */ + public static void transformToWindowSpaceRect(Activity activity, Rect inOutRect) { + Rect windowRect = getWindowBounds(activity); if (windowRect == null) { inOutRect.setEmpty(); return; @@ -95,32 +101,17 @@ class SidecarHelper { } /** - * Get the current window bounds in absolute coordinates. - * NOTE: Only works with Activity windows. + * Gets the current window bounds in absolute coordinates. */ @Nullable - private static Rect getWindowBounds(IBinder windowToken) { - Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken); - return activity != null - ? activity.getWindowManager().getCurrentWindowMetrics().getBounds() - : null; - } - - /** - * Check if this window is an Activity window that is in multi-window mode. - */ - static boolean isInMultiWindow(IBinder windowToken) { - Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken); - return activity != null && activity.isInMultiWindowMode(); + private static Rect getWindowBounds(@NonNull Activity activity) { + return activity.getWindowManager().getCurrentWindowMetrics().getBounds(); } /** - * Get the id of the parent display for the window. - * NOTE: Only works with Activity windows. + * Checks if both dimensions of the given rect are zero at the same time. */ - static int getWindowDisplay(IBinder windowToken) { - Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken); - return activity != null - ? activity.getWindowManager().getDefaultDisplay().getDisplayId() : INVALID_DISPLAY; + public static boolean isZero(@NonNull Rect rect) { + return rect.height() == 0 && rect.width() == 0; } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/SettingsConfigProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/util/SettingsConfigProvider.java new file mode 100644 index 000000000000..6dd190ce2519 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/SettingsConfigProvider.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2021 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 androidx.window.util; + +import static androidx.window.util.ExtensionHelper.isZero; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.graphics.Rect; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.internal.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Device and display feature state provider that uses Settings as the source. + */ +public final class SettingsConfigProvider extends ContentObserver { + private static final String TAG = "SettingsConfigProvider"; + private static final String DEVICE_POSTURE = "device_posture"; + private static final String DISPLAY_FEATURES = "display_features"; + + private static final Pattern FEATURE_PATTERN = + Pattern.compile("([a-z]+)-\\[(\\d+),(\\d+),(\\d+),(\\d+)]"); + + private static final String FEATURE_TYPE_FOLD = "fold"; + private static final String FEATURE_TYPE_HINGE = "hinge"; + + private final Uri mDevicePostureUri = + Settings.Global.getUriFor(DEVICE_POSTURE); + private final Uri mDisplayFeaturesUri = + Settings.Global.getUriFor(DISPLAY_FEATURES); + private final Context mContext; + private final ContentResolver mResolver; + private final StateChangeCallback mCallback; + private boolean mRegisteredObservers; + + public SettingsConfigProvider(@NonNull Context context, @NonNull StateChangeCallback callback) { + super(new Handler(Looper.getMainLooper())); + mContext = context; + mResolver = context.getContentResolver(); + mCallback = callback; + } + + /** + * Registers the content observers for Settings keys that store device state and display feature + * configurations. + */ + public void registerObserversIfNeeded() { + if (mRegisteredObservers) { + return; + } + mRegisteredObservers = true; + mResolver.registerContentObserver(mDevicePostureUri, false /* notifyForDescendants */, + this /* ContentObserver */); + mResolver.registerContentObserver(mDisplayFeaturesUri, false /* notifyForDescendants */, + this /* ContentObserver */); + } + + /** + * Unregisters the content observers that are tracking the state changes. + * @see #registerObserversIfNeeded() + */ + public void unregisterObserversIfNeeded() { + if (!mRegisteredObservers) { + return; + } + mRegisteredObservers = false; + mResolver.unregisterContentObserver(this); + } + + /** + * Gets the device posture int stored in Settings. + */ + public int getDeviceState() { + return Settings.Global.getInt(mResolver, DEVICE_POSTURE, + 0 /* POSTURE_UNKNOWN */); + } + + /** + * Gets the list of all display feature configs stored in Settings. Uses a custom + * {@link BaseDisplayFeature} class to report the config to be translated for actual + * containers in Sidecar or Extensions. + */ + public List<BaseDisplayFeature> getDisplayFeatures() { + List<BaseDisplayFeature> features = new ArrayList<>(); + String displayFeaturesString = Settings.Global.getString(mResolver, DISPLAY_FEATURES); + if (TextUtils.isEmpty(displayFeaturesString)) { + displayFeaturesString = mContext.getResources().getString( + R.string.config_display_features); + } + if (TextUtils.isEmpty(displayFeaturesString)) { + return features; + } + String[] featureStrings = displayFeaturesString.split(";"); + + int deviceState = getDeviceState(); + + for (String featureString : featureStrings) { + Matcher featureMatcher = FEATURE_PATTERN.matcher(featureString); + if (!featureMatcher.matches()) { + Log.e(TAG, "Malformed feature description format: " + featureString); + continue; + } + try { + String featureType = featureMatcher.group(1); + int type; + switch (featureType) { + case FEATURE_TYPE_FOLD: + type = 1 /* TYPE_FOLD */; + break; + case FEATURE_TYPE_HINGE: + type = 2 /* TYPE_HINGE */; + break; + default: { + Log.e(TAG, "Malformed feature type: " + featureType); + continue; + } + } + + int left = Integer.parseInt(featureMatcher.group(2)); + int top = Integer.parseInt(featureMatcher.group(3)); + int right = Integer.parseInt(featureMatcher.group(4)); + int bottom = Integer.parseInt(featureMatcher.group(5)); + Rect featureRect = new Rect(left, top, right, bottom); + if (!isZero(featureRect)) { + BaseDisplayFeature feature = new BaseDisplayFeature(type, deviceState, + featureRect); + features.add(feature); + } else { + Log.w(TAG, "Read empty feature"); + } + } catch (NumberFormatException e) { + Log.e(TAG, "Malformed feature description: " + featureString); + } + } + return features; + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (uri == null) { + return; + } + + if (mDevicePostureUri.equals(uri)) { + mCallback.onDevicePostureChanged(); + mCallback.onDisplayFeaturesChanged(); + return; + } + if (mDisplayFeaturesUri.equals(uri)) { + mCallback.onDisplayFeaturesChanged(); + } + } + + /** + * Callback that notifies about device or display feature state changes. + */ + public interface StateChangeCallback { + /** + * Notifies about the device state update. + */ + void onDevicePostureChanged(); + + /** + * Notifies about the display feature config update. + */ + void onDisplayFeaturesChanged(); + } +} diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar Binary files differnew file mode 100644 index 000000000000..7b306b00afd9 --- /dev/null +++ b/libs/WindowManager/Jetpack/window-extensions-release.aar diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml new file mode 100644 index 000000000000..73a48d31a814 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2019 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#aa000000" + android:pathData="M0,12 a12,12 0 1,0 24,0 a12,12 0 1,0 -24,0" /> + <path + android:fillColor="@android:color/white" + android:pathData="M17.65,6.35c-1.63,-1.63 -3.94,-2.57 -6.48,-2.31c-3.67,0.37 -6.69,3.35 -7.1,7.02C3.52,15.91 7.27,20 12,20c3.19,0 5.93,-1.87 7.21,-4.57c0.31,-0.66 -0.16,-1.43 -0.89,-1.43h-0.01c-0.37,0 -0.72,0.2 -0.88,0.53c-1.13,2.43 -3.84,3.97 -6.81,3.32c-2.22,-0.49 -4.01,-2.3 -4.49,-4.52C5.31,9.44 8.26,6 12,6c1.66,0 3.14,0.69 4.22,1.78l-2.37,2.37C13.54,10.46 13.76,11 14.21,11H19c0.55,0 1,-0.45 1,-1V5.21c0,-0.45 -0.54,-0.67 -0.85,-0.35L17.65,6.35z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml b/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml new file mode 100644 index 000000000000..347c2b47767e --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2019 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@android:color/background_light" + android:orientation="vertical"> + + <TextView + android:layout_width="180dp" + android:layout_height="wrap_content" + android:paddingLeft="10dp" + android:paddingRight="10dp" + android:paddingTop="10dp" + android:text="@string/restart_button_description" + android:textAlignment="viewStart" + android:textColor="@android:color/primary_text_light" + android:textSize="16sp" /> + + <Button + android:id="@+id/got_it" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:includeFontPadding="false" + android:layout_gravity="end" + android:minHeight="36dp" + android:background="?android:attr/selectableItemBackground" + android:text="@string/got_it" + android:textAllCaps="true" + android:textColor="#3c78d8" + android:textSize="16sp" + android:textStyle="bold" /> + +</LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/size_compat_ui.xml b/libs/WindowManager/Shell/res/layout/size_compat_ui.xml new file mode 100644 index 000000000000..cd3153145be3 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/size_compat_ui.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<com.android.wm.shell.sizecompatui.SizeCompatRestartButton + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <ImageButton + android:id="@+id/size_compat_restart_button" + android:layout_width="@dimen/size_compat_button_size" + android:layout_height="@dimen/size_compat_button_size" + android:layout_gravity="center" + android:src="@drawable/size_compat_restart_button" + android:contentDescription="@string/restart_button_description"/> + +</com.android.wm.shell.sizecompatui.SizeCompatRestartButton> diff --git a/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json index 2cfb13e7dea6..9c3d84e72f8c 100644 --- a/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json +++ b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json @@ -13,6 +13,12 @@ "group": "WM_SHELL_TASK_ORG", "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" }, + "-1671119352": { + "message": " Delegate animation for %s to %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/transition\/RemoteTransitionHandler.java" + }, "-1501874464": { "message": "Fullscreen Task Appeared: #%d", "level": "VERBOSE", @@ -49,6 +55,24 @@ "group": "WM_SHELL_TASK_ORG", "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" }, + "-1308483871": { + "message": " try handler %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/transition\/Transitions.java" + }, + "-1297259344": { + "message": " animated by %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/transition\/Transitions.java" + }, + "-1269886472": { + "message": "Transition %s doesn't have explicit remote, search filters for match for %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/transition\/RemoteTransitionHandler.java" + }, "-1006733970": { "message": "Display added: %d", "level": "VERBOSE", @@ -91,12 +115,24 @@ "group": "WM_SHELL_TASK_ORG", "at": "com\/android\/wm\/shell\/apppairs\/AppPairsController.java" }, + "138343607": { + "message": " try firstHandler %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/transition\/Transitions.java" + }, "157713005": { "message": "Task info changed taskId=%d", "level": "VERBOSE", "group": "WM_SHELL_TASK_ORG", "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" }, + "214412327": { + "message": "RemoteTransition directly requested for %s: %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/transition\/RemoteTransitionHandler.java" + }, "274140888": { "message": "Animate alpha: from=%d to=%d", "level": "VERBOSE", @@ -115,6 +151,12 @@ "group": "WM_SHELL_DRAG_AND_DROP", "at": "com\/android\/wm\/shell\/draganddrop\/DragAndDropController.java" }, + "410592459": { + "message": "Invalid root leash (%s): %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/transition\/Transitions.java" + }, "473543554": { "message": "%s onTaskAppeared Supported", "level": "VERBOSE", @@ -139,6 +181,12 @@ "group": "WM_SHELL_TASK_ORG", "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" }, + "707170340": { + "message": " animated by firstHandler", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/transition\/Transitions.java" + }, "900599280": { "message": "Can't pair unresizeable tasks task1.isResizeable=%b task1.isResizeable=%b", "level": "ERROR", @@ -163,6 +211,12 @@ "group": "WM_SHELL_TASK_ORG", "at": "com\/android\/wm\/shell\/legacysplitscreen\/LegacySplitScreenTaskListener.java" }, + "990371881": { + "message": " Checking filter %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/transition\/RemoteTransitionHandler.java" + }, "1070270131": { "message": "onTransitionReady %s: %s", "level": "VERBOSE", diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 13f1fddfdfb6..24198659e15d 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -48,4 +48,7 @@ <!-- one handed background panel default alpha --> <item name="config_one_handed_background_alpha" format="float" type="dimen">0.5</item> + + <!-- maximum animation duration for the icon when entering the starting window --> + <integer name="max_starting_window_intro_icon_anim_duration">1000</integer> </resources> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 034e65c608a3..583964b2f4a4 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -173,4 +173,13 @@ <!-- The width/height of the icon view on staring surface. --> <dimen name="starting_surface_icon_size">108dp</dimen> + + <!-- The width/height of the size compat restart button. --> + <dimen name="size_compat_button_size">48dp</dimen> + + <!-- The width of the brand image on staring surface. --> + <dimen name="starting_surface_brand_image_width">200dp</dimen> + + <!-- The height of the brand image on staring surface. --> + <dimen name="starting_surface_brand_image_height">80dp</dimen> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 30ef72c2a352..b1425e4eeb28 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -146,4 +146,10 @@ <!-- Content description to tell the user a bubble has been dismissed. --> <string name="accessibility_bubble_dismissed">Bubble dismissed.</string> + + <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] --> + <string name="restart_button_description">Tap to restart this app and go full screen.</string> + + <!-- Generic "got it" acceptance of dialog or cling [CHAR LIMIT=NONE] --> + <string name="got_it">Got it</string> </resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java index 4b3fc811f7eb..6984ea458ccf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java @@ -95,9 +95,16 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { } @Override + public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { + if (!mLeashByTaskId.contains(taskId)) { + throw new IllegalArgumentException("There is no surface for taskId=" + taskId); + } + b.setParent(mLeashByTaskId.get(taskId)); + } + + @Override public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; - final String childPrefix = innerPrefix + " "; pw.println(prefix + this); pw.println(innerPrefix + mLeashByTaskId.size() + " Tasks"); } @@ -106,5 +113,4 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { public String toString() { return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_FULLSCREEN); } - } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java index aa82339a436a..73fd6931066d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java @@ -16,24 +16,16 @@ package com.android.wm.shell; -import android.util.Slog; - -import com.android.wm.shell.apppairs.AppPairs; -import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; -import com.android.wm.shell.onehanded.OneHanded; -import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.common.annotations.ExternalThread; import java.io.PrintWriter; -import java.util.Optional; -import java.util.concurrent.TimeUnit; /** * An entry point into the shell for dumping shell internal state and running adb commands. * * Use with {@code adb shell dumpsys activity service SystemUIService WMShell ...}. */ +@ExternalThread public interface ShellCommandHandler { /** * Dumps the shell state. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java index 52648d915f2c..eaed24d6195a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java @@ -16,15 +16,15 @@ package com.android.wm.shell; -import static com.android.wm.shell.splitscreen.SplitScreen.SIDE_STAGE_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_BOTTOM_OR_RIGHT; -import com.android.wm.shell.apppairs.AppPairs; +import com.android.wm.shell.apppairs.AppPairsController; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; -import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.pip.Pip; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; -import com.android.wm.shell.splitscreen.SplitScreen; +import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; +import com.android.wm.shell.onehanded.OneHandedController; +import com.android.wm.shell.splitscreen.SplitScreenController; import java.io.PrintWriter; import java.util.Optional; @@ -37,24 +37,24 @@ import java.util.Optional; public final class ShellCommandHandlerImpl { private static final String TAG = ShellCommandHandlerImpl.class.getSimpleName(); - private final Optional<LegacySplitScreen> mLegacySplitScreenOptional; - private final Optional<SplitScreen> mSplitScreenOptional; + private final Optional<LegacySplitScreenController> mLegacySplitScreenOptional; + private final Optional<SplitScreenController> mSplitScreenOptional; private final Optional<Pip> mPipOptional; - private final Optional<OneHanded> mOneHandedOptional; - private final Optional<HideDisplayCutout> mHideDisplayCutout; + private final Optional<OneHandedController> mOneHandedOptional; + private final Optional<HideDisplayCutoutController> mHideDisplayCutout; + private final Optional<AppPairsController> mAppPairsOptional; private final ShellTaskOrganizer mShellTaskOrganizer; - private final Optional<AppPairs> mAppPairsOptional; private final ShellExecutor mMainExecutor; private final HandlerImpl mImpl = new HandlerImpl(); public static ShellCommandHandler create( ShellTaskOrganizer shellTaskOrganizer, - Optional<LegacySplitScreen> legacySplitScreenOptional, - Optional<SplitScreen> splitScreenOptional, + Optional<LegacySplitScreenController> legacySplitScreenOptional, + Optional<SplitScreenController> splitScreenOptional, Optional<Pip> pipOptional, - Optional<OneHanded> oneHandedOptional, - Optional<HideDisplayCutout> hideDisplayCutout, - Optional<AppPairs> appPairsOptional, + Optional<OneHandedController> oneHandedOptional, + Optional<HideDisplayCutoutController> hideDisplayCutout, + Optional<AppPairsController> appPairsOptional, ShellExecutor mainExecutor) { return new ShellCommandHandlerImpl(shellTaskOrganizer, legacySplitScreenOptional, splitScreenOptional, pipOptional, oneHandedOptional, hideDisplayCutout, @@ -63,12 +63,12 @@ public final class ShellCommandHandlerImpl { private ShellCommandHandlerImpl( ShellTaskOrganizer shellTaskOrganizer, - Optional<LegacySplitScreen> legacySplitScreenOptional, - Optional<SplitScreen> splitScreenOptional, + Optional<LegacySplitScreenController> legacySplitScreenOptional, + Optional<SplitScreenController> splitScreenOptional, Optional<Pip> pipOptional, - Optional<OneHanded> oneHandedOptional, - Optional<HideDisplayCutout> hideDisplayCutout, - Optional<AppPairs> appPairsOptional, + Optional<OneHandedController> oneHandedOptional, + Optional<HideDisplayCutoutController> hideDisplayCutout, + Optional<AppPairsController> appPairsOptional, ShellExecutor mainExecutor) { mShellTaskOrganizer = shellTaskOrganizer; mLegacySplitScreenOptional = legacySplitScreenOptional; @@ -155,7 +155,7 @@ public final class ShellCommandHandlerImpl { } final int taskId = new Integer(args[2]); final int sideStagePosition = args.length > 3 - ? new Integer(args[3]) : SIDE_STAGE_POSITION_BOTTOM_OR_RIGHT; + ? new Integer(args[3]) : STAGE_POSITION_BOTTOM_OR_RIGHT; mSplitScreenOptional.ifPresent(split -> split.moveToSideStage(taskId, sideStagePosition)); return true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java index 0958a070c82d..7376d9898ab8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java @@ -18,13 +18,13 @@ package com.android.wm.shell; import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN; -import com.android.wm.shell.apppairs.AppPairs; +import com.android.wm.shell.apppairs.AppPairsController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.draganddrop.DragAndDropController; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; -import com.android.wm.shell.splitscreen.SplitScreen; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; +import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.transition.Transitions; import java.util.Optional; @@ -38,9 +38,9 @@ public class ShellInitImpl { private final DisplayImeController mDisplayImeController; private final DragAndDropController mDragAndDropController; private final ShellTaskOrganizer mShellTaskOrganizer; - private final Optional<LegacySplitScreen> mLegacySplitScreenOptional; - private final Optional<SplitScreen> mSplitScreenOptional; - private final Optional<AppPairs> mAppPairsOptional; + private final Optional<LegacySplitScreenController> mLegacySplitScreenOptional; + private final Optional<SplitScreenController> mSplitScreenOptional; + private final Optional<AppPairsController> mAppPairsOptional; private final FullscreenTaskListener mFullscreenTaskListener; private final ShellExecutor mMainExecutor; private final Transitions mTransitions; @@ -50,9 +50,9 @@ public class ShellInitImpl { public static ShellInit create(DisplayImeController displayImeController, DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, - Optional<LegacySplitScreen> legacySplitScreenOptional, - Optional<SplitScreen> splitScreenOptional, - Optional<AppPairs> appPairsOptional, + Optional<LegacySplitScreenController> legacySplitScreenOptional, + Optional<SplitScreenController> splitScreenOptional, + Optional<AppPairsController> appPairsOptional, FullscreenTaskListener fullscreenTaskListener, Transitions transitions, ShellExecutor mainExecutor) { @@ -70,9 +70,9 @@ public class ShellInitImpl { private ShellInitImpl(DisplayImeController displayImeController, DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, - Optional<LegacySplitScreen> legacySplitScreenOptional, - Optional<SplitScreen> splitScreenOptional, - Optional<AppPairs> appPairsOptional, + Optional<LegacySplitScreenController> legacySplitScreenOptional, + Optional<SplitScreenController> splitScreenOptional, + Optional<AppPairsController> appPairsOptional, FullscreenTaskListener fullscreenTaskListener, Transitions transitions, ShellExecutor mainExecutor) { @@ -96,8 +96,8 @@ public class ShellInitImpl { // Register the shell organizer mShellTaskOrganizer.registerOrganizer(); - mAppPairsOptional.ifPresent(AppPairs::onOrganizerRegistered); - mSplitScreenOptional.ifPresent(SplitScreen::onOrganizerRegistered); + mAppPairsOptional.ifPresent(AppPairsController::onOrganizerRegistered); + mSplitScreenOptional.ifPresent(SplitScreenController::onOrganizerRegistered); // Bind the splitscreen impl to the drag drop controller mDragAndDropController.initialize(mSplitScreenOptional); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index c9b38d00c0ae..efc55c4fbe31 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -25,6 +25,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.os.Binder; @@ -38,12 +40,10 @@ import android.window.StartingWindowInfo; import android.window.TaskAppearedInfo; import android.window.TaskOrganizer; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.sizecompatui.SizeCompatUIController; import com.android.wm.shell.startingsurface.StartingSurfaceDrawer; import java.io.PrintWriter; @@ -82,6 +82,16 @@ public class ShellTaskOrganizer extends TaskOrganizer { default void onTaskInfoChanged(RunningTaskInfo taskInfo) {} default void onTaskVanished(RunningTaskInfo taskInfo) {} default void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) {} + /** Whether this task listener supports size compat UI. */ + default boolean supportSizeCompatUI() { + // All TaskListeners should support size compat except PIP. + return true; + } + /** Attaches the a child window surface to the task surface. */ + default void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { + throw new IllegalStateException( + "This task listener doesn't support child surface attachment."); + } default void dump(@NonNull PrintWriter pw, String prefix) {}; } @@ -102,18 +112,31 @@ public class ShellTaskOrganizer extends TaskOrganizer { private final Object mLock = new Object(); private final StartingSurfaceDrawer mStartingSurfaceDrawer; + /** + * In charge of showing size compat UI. Can be {@code null} if device doesn't support size + * compat. + */ + @Nullable + private final SizeCompatUIController mSizeCompatUI; + public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context) { - this(null, mainExecutor, context); + this(null /* taskOrganizerController */, mainExecutor, context, null /* sizeCompatUI */, + new StartingSurfaceDrawer(context, mainExecutor)); + } + + public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable + SizeCompatUIController sizeCompatUI) { + this(null /* taskOrganizerController */, mainExecutor, context, sizeCompatUI, + new StartingSurfaceDrawer(context, mainExecutor)); } @VisibleForTesting ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, ShellExecutor mainExecutor, - Context context) { + Context context, @Nullable SizeCompatUIController sizeCompatUI, + StartingSurfaceDrawer startingSurfaceDrawer) { super(taskOrganizerController, mainExecutor); - // TODO(b/131727939) temporarily live here, the starting surface drawer should be controlled - // by a controller, that class should be create while porting - // ActivityRecord#addStartingWindow to WMShell. - mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, mainExecutor); + mSizeCompatUI = sizeCompatUI; + mStartingSurfaceDrawer = startingSurfaceDrawer; } @Override @@ -240,6 +263,11 @@ public class ShellTaskOrganizer extends TaskOrganizer { } @Override + public void copySplashScreenView(int taskId) { + mStartingSurfaceDrawer.copySplashScreenView(taskId); + } + + @Override public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { synchronized (mLock) { onTaskAppeared(new TaskAppearedInfo(taskInfo, leash)); @@ -255,6 +283,7 @@ public class ShellTaskOrganizer extends TaskOrganizer { if (listener != null) { listener.onTaskAppeared(info.getTaskInfo(), info.getLeash()); } + notifySizeCompatUI(info.getTaskInfo(), listener); } @Override @@ -270,6 +299,10 @@ public class ShellTaskOrganizer extends TaskOrganizer { if (!updated && newListener != null) { newListener.onTaskInfoChanged(taskInfo); } + if (updated || !taskInfo.equalsForSizeCompat(data.getTaskInfo())) { + // Notify the size compat UI if the listener or task info changed. + notifySizeCompatUI(taskInfo, newListener); + } } } @@ -294,6 +327,8 @@ public class ShellTaskOrganizer extends TaskOrganizer { if (listener != null) { listener.onTaskVanished(taskInfo); } + // Pass null for listener to remove the size compat UI on this task if there is any. + notifySizeCompatUI(taskInfo, null /* taskListener */); } } @@ -320,6 +355,33 @@ public class ShellTaskOrganizer extends TaskOrganizer { return true; } + /** + * Notifies {@link SizeCompatUIController} about the size compat info changed on the give Task + * to update the UI accordingly. + * + * @param taskInfo the new Task info + * @param taskListener listener to handle the Task Surface placement. {@code null} if task is + * vanished. + */ + private void notifySizeCompatUI(RunningTaskInfo taskInfo, @Nullable TaskListener taskListener) { + if (mSizeCompatUI == null) { + return; + } + + // The task is vanished or doesn't support size compat UI, notify to remove size compat UI + // on this Task if there is any. + if (taskListener == null || !taskListener.supportSizeCompatUI() + || !taskInfo.topActivityInSizeCompat) { + mSizeCompatUI.onSizeCompatInfoChanged(taskInfo.displayId, taskInfo.taskId, + null /* taskConfig */, null /* sizeCompatActivity*/, + null /* taskListener */); + return; + } + + mSizeCompatUI.onSizeCompatInfoChanged(taskInfo.displayId, taskInfo.taskId, + taskInfo.configuration, taskInfo.topActivityToken, taskListener); + } + private TaskListener getTaskListener(RunningTaskInfo runningTaskInfo) { return getTaskListener(runningTaskInfo, false /*removeLaunchCookieIfNeeded*/); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java index bb8a97344664..5992447bd6da 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java @@ -301,6 +301,14 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, } @Override + public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { + if (mTaskInfo.taskId != taskId) { + throw new IllegalArgumentException("There is no surface for taskId=" + taskId); + } + b.setParent(mTaskLeash); + } + + @Override public void dump(@androidx.annotation.NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; final String childPrefix = innerPrefix + " "; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java index a5dd79b373bd..58ca1fbaba24 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java @@ -30,6 +30,7 @@ import java.util.function.Consumer; public class TaskViewFactoryController { private final ShellTaskOrganizer mTaskOrganizer; private final ShellExecutor mShellExecutor; + private final TaskViewFactory mImpl = new TaskViewFactoryImpl(); public TaskViewFactoryController(ShellTaskOrganizer taskOrganizer, ShellExecutor shellExecutor) { @@ -37,8 +38,11 @@ public class TaskViewFactoryController { mShellExecutor = shellExecutor; } + public TaskViewFactory asTaskViewFactory() { + return mImpl; + } + /** Creates an {@link TaskView} */ - @ShellMainThread public void create(@UiContext Context context, Executor executor, Consumer<TaskView> onCreate) { TaskView taskView = new TaskView(context, mTaskOrganizer); executor.execute(() -> { @@ -46,10 +50,6 @@ public class TaskViewFactoryController { }); } - public TaskViewFactory getTaskViewFactory() { - return new TaskViewFactoryImpl(); - } - private class TaskViewFactoryImpl implements TaskViewFactory { @ExternalThread public void create(@UiContext Context context, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java index abd92577c1d4..59271e9fb63c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java @@ -27,6 +27,7 @@ import com.android.wm.shell.pip.PinnedStackListenerForwarder.PinnedStackListener /** * The singleton wrapper to communicate between WindowManagerService and WMShell features * (e.g: PIP, SplitScreen, Bubble, OneHandedMode...etc) + * TODO: Remove once PinnedStackListenerForwarder can be removed */ public class WindowManagerShellWrapper { private static final String TAG = WindowManagerShellWrapper.class.getSimpleName(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt index 7ea4689be7e2..255e4d2c0d44 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.animation -import android.os.Looper import android.util.ArrayMap import android.util.Log import android.view.View @@ -847,7 +846,7 @@ class PhysicsAnimator<T> private constructor (target: T) { * pass to [spring]. */ data class SpringConfig internal constructor( - internal var stiffness: Float, + var stiffness: Float, internal var dampingRatio: Float, internal var startVelocity: Float = 0f, internal var finalPosition: Float = UNSET @@ -879,8 +878,8 @@ class PhysicsAnimator<T> private constructor (target: T) { */ data class FlingConfig internal constructor( internal var friction: Float, - internal var min: Float, - internal var max: Float, + var min: Float, + var max: Float, internal var startVelocity: Float ) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java index bab5140e2f52..79f9dcd8a1fb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java @@ -214,6 +214,19 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.LayoutChan } @Override + public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { + if (getRootTaskId() == taskId) { + b.setParent(mRootTaskLeash); + } else if (getTaskId1() == taskId) { + b.setParent(mTaskLeash1); + } else if (getTaskId2() == taskId) { + b.setParent(mTaskLeash2); + } else { + throw new IllegalArgumentException("There is no surface for taskId=" + taskId); + } + } + + @Override public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; final String childPrefix = innerPrefix + " "; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java index f5aa852c87ae..a9b1dbc3c23b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java @@ -35,8 +35,4 @@ public interface AppPairs { boolean pair(ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2); /** Unpairs any app-pair containing this task id. */ void unpair(int taskId); - /** Dumps current status of app pairs. */ - void dump(@NonNull PrintWriter pw, String prefix); - /** Called when the shell organizer has been registered. */ - void onOrganizerRegistered(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java index e380426b9ca2..0415f12496f2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java @@ -51,18 +51,7 @@ public class AppPairsController { private final SparseArray<AppPair> mActiveAppPairs = new SparseArray<>(); private final DisplayController mDisplayController; - /** - * Creates {@link AppPairs}, returns {@code null} if the feature is not supported. - */ - @Nullable - public static AppPairs create(ShellTaskOrganizer organizer, - SyncTransactionQueue syncQueue, DisplayController displayController, - ShellExecutor mainExecutor) { - return new AppPairsController(organizer, syncQueue, displayController, - mainExecutor).mImpl; - } - - AppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue, + public AppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue, DisplayController displayController, ShellExecutor mainExecutor) { mTaskOrganizer = organizer; mSyncQueue = syncQueue; @@ -70,18 +59,22 @@ public class AppPairsController { mMainExecutor = mainExecutor; } - void onOrganizerRegistered() { + public AppPairs asAppPairs() { + return mImpl; + } + + public void onOrganizerRegistered() { if (mPairsPool == null) { setPairsPool(new AppPairsPool(this)); } } @VisibleForTesting - void setPairsPool(AppPairsPool pool) { + public void setPairsPool(AppPairsPool pool) { mPairsPool = pool; } - boolean pair(int taskId1, int taskId2) { + public boolean pair(int taskId1, int taskId2) { final ActivityManager.RunningTaskInfo task1 = mTaskOrganizer.getRunningTaskInfo(taskId1); final ActivityManager.RunningTaskInfo task2 = mTaskOrganizer.getRunningTaskInfo(taskId2); if (task1 == null || task2 == null) { @@ -90,13 +83,13 @@ public class AppPairsController { return pair(task1, task2); } - boolean pair(ActivityManager.RunningTaskInfo task1, + public boolean pair(ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) { return pairInner(task1, task2) != null; } @VisibleForTesting - AppPair pairInner( + public AppPair pairInner( @NonNull ActivityManager.RunningTaskInfo task1, @NonNull ActivityManager.RunningTaskInfo task2) { final AppPair pair = mPairsPool.acquire(); @@ -109,11 +102,11 @@ public class AppPairsController { return pair; } - void unpair(int taskId) { + public void unpair(int taskId) { unpair(taskId, true /* releaseToPool */); } - void unpair(int taskId, boolean releaseToPool) { + public void unpair(int taskId, boolean releaseToPool) { AppPair pair = mActiveAppPairs.get(taskId); if (pair == null) { for (int i = mActiveAppPairs.size() - 1; i >= 0; --i) { @@ -137,19 +130,19 @@ public class AppPairsController { } } - ShellTaskOrganizer getTaskOrganizer() { + public ShellTaskOrganizer getTaskOrganizer() { return mTaskOrganizer; } - SyncTransactionQueue getSyncTransactionQueue() { + public SyncTransactionQueue getSyncTransactionQueue() { return mSyncQueue; } - DisplayController getDisplayController() { + public DisplayController getDisplayController() { return mDisplayController; } - private void dump(@NonNull PrintWriter pw, String prefix) { + public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; final String childPrefix = innerPrefix + " "; pw.println(prefix + this); @@ -202,21 +195,5 @@ public class AppPairsController { AppPairsController.this.unpair(taskId); }); } - - @Override - public void onOrganizerRegistered() { - mMainExecutor.execute(() -> { - AppPairsController.this.onOrganizerRegistered(); - }); - } - - @Override - public void dump(@NonNull PrintWriter pw, String prefix) { - try { - mMainExecutor.executeBlocking(() -> AppPairsController.this.dump(pw, prefix)); - } catch (InterruptedException e) { - Slog.e(TAG, "Failed to dump AppPairsController in 2s"); - } - } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index ffeabd876b81..0ee1f0642352 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -39,6 +39,7 @@ import android.graphics.drawable.Icon; import android.os.Parcelable; import android.os.UserHandle; import android.provider.Settings; +import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.Log; @@ -59,6 +60,8 @@ public class Bubble implements BubbleViewProvider { private static final String TAG = "Bubble"; private final String mKey; + @Nullable + private final String mGroupKey; private final Executor mMainExecutor; private long mLastUpdated; @@ -165,6 +168,7 @@ public class Bubble implements BubbleViewProvider { mMetadataShortcutId = shortcutInfo.getId(); mShortcutInfo = shortcutInfo; mKey = key; + mGroupKey = null; mFlags = 0; mUser = shortcutInfo.getUserHandle(); mPackageName = shortcutInfo.getPackage(); @@ -182,6 +186,7 @@ public class Bubble implements BubbleViewProvider { final Bubbles.PendingIntentCanceledListener intentCancelListener, Executor mainExecutor) { mKey = entry.getKey(); + mGroupKey = entry.getGroupKey(); mSuppressionListener = listener; mIntentCancelListener = intent -> { if (mIntent != null) { @@ -200,6 +205,14 @@ public class Bubble implements BubbleViewProvider { return mKey; } + /** + * @see StatusBarNotification#getGroupKey() + * @return the group key for this bubble, if one exists. + */ + public String getGroupKey() { + return mGroupKey; + } + public UserHandle getUser() { return mUser; } @@ -385,6 +398,14 @@ public class Bubble implements BubbleViewProvider { } } + @Override + public void setExpandedContentAlpha(float alpha) { + if (mExpandedView != null) { + mExpandedView.setAlpha(alpha); + mExpandedView.setTaskViewAlpha(alpha); + } + } + /** * Set visibility of bubble in the expanded state. * @@ -394,7 +415,7 @@ public class Bubble implements BubbleViewProvider { * and setting {@code false} actually means rendering the expanded view in transparent. */ @Override - public void setContentVisibility(boolean visibility) { + public void setTaskViewVisibility(boolean visibility) { if (mExpandedView != null) { mExpandedView.setContentVisibility(visibility); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 7538c8b7ffad..047df5ba7ca9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -86,6 +86,7 @@ import com.android.wm.shell.pip.PinnedStackListenerForwarder; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; @@ -192,9 +193,9 @@ public class BubbleController { private boolean mIsStatusBarShade = true; /** - * Injected constructor. + * Creates an instance of the BubbleController. */ - public static Bubbles create(Context context, + public static BubbleController create(Context context, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, @Nullable IStatusBarService statusBarService, @@ -211,14 +212,14 @@ public class BubbleController { return new BubbleController(context, data, synchronizer, floatingContentCoordinator, new BubbleDataRepository(context, launcherApps, mainExecutor), statusBarService, windowManager, windowManagerShellWrapper, launcherApps, - logger, organizer, positioner, mainExecutor, mainHandler).mImpl; + logger, organizer, positioner, mainExecutor, mainHandler); } /** * Testing constructor. */ @VisibleForTesting - public BubbleController(Context context, + protected BubbleController(Context context, BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, @@ -322,7 +323,7 @@ public class BubbleController { } @VisibleForTesting - public Bubbles getImpl() { + public Bubbles asBubbles() { return mImpl; } @@ -586,11 +587,15 @@ public class BubbleController { // There were no bubbles saved for this used. return; } - for (BubbleEntry e : mSysuiProxy.getShouldRestoredEntries(savedBubbleKeys)) { - if (canLaunchInActivityView(mContext, e)) { - updateBubble(e, true /* suppressFlyout */, false /* showInShade */); - } - } + mSysuiProxy.getShouldRestoredEntries(savedBubbleKeys, (entries) -> { + mMainExecutor.execute(() -> { + for (BubbleEntry e : entries) { + if (canLaunchInActivityView(mContext, e)) { + updateBubble(e, true /* suppressFlyout */, false /* showInShade */); + } + } + }); + }); // Finally, remove the entries for this user now that bubbles are restored. mSavedBubbleKeysPerUser.remove(mCurrentUserId); } @@ -771,7 +776,8 @@ public class BubbleController { // if the bubble is already active, there's no need to push it to overflow return; } - bubble.inflate((b) -> mBubbleData.overflowBubble(DISMISS_AGED, bubble), + bubble.inflate( + (b) -> mBubbleData.overflowBubble(Bubbles.DISMISS_RELOAD_FROM_DISK, bubble), mContext, this, mStackView, mBubbleIconFactory, true /* skipInflation */); }); return null; @@ -855,21 +861,24 @@ public class BubbleController { } } - private void onRankingUpdated(RankingMap rankingMap) { + private void onRankingUpdated(RankingMap rankingMap, + HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) { if (mTmpRanking == null) { mTmpRanking = new NotificationListenerService.Ranking(); } String[] orderedKeys = rankingMap.getOrderedKeys(); for (int i = 0; i < orderedKeys.length; i++) { String key = orderedKeys[i]; - BubbleEntry entry = mSysuiProxy.getPendingOrActiveEntry(key); + Pair<BubbleEntry, Boolean> entryData = entryDataByKey.get(key); + BubbleEntry entry = entryData.first; + boolean shouldBubbleUp = entryData.second; rankingMap.getRanking(key, mTmpRanking); boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key); if (isActiveBubble && !mTmpRanking.canBubble()) { // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason. // This means that the app or channel's ability to bubble has been revoked. mBubbleData.dismissBubbleWithKey(key, DISMISS_BLOCKED); - } else if (isActiveBubble && !mSysuiProxy.shouldBubbleUp(key)) { + } else if (isActiveBubble && !shouldBubbleUp) { // If this entry is allowed to bubble, but cannot currently bubble up, dismiss it. // This happens when DND is enabled and configured to hide bubbles. Dismissing with // the reason DISMISS_NO_BUBBLE_UP will retain the underlying notification, so that @@ -892,10 +901,7 @@ public class BubbleController { return bubbleChildren; } for (Bubble bubble : mBubbleData.getActiveBubbles()) { - // TODO(178620678): Prevent calling into SysUI since this can be a part of a blocking - // call from SysUI to Shell - final BubbleEntry entry = mSysuiProxy.getPendingOrActiveEntry(bubble.getKey()); - if (entry != null && groupKey.equals(entry.getStatusBarNotification().getGroupKey())) { + if (bubble.getGroupKey() != null && groupKey.equals(bubble.getGroupKey())) { bubbleChildren.add(bubble); } } @@ -921,17 +927,20 @@ public class BubbleController { private void setIsBubble(@NonNull final Bubble b, final boolean isBubble) { Objects.requireNonNull(b); b.setIsBubble(isBubble); - final BubbleEntry entry = mSysuiProxy.getPendingOrActiveEntry(b.getKey()); - if (entry != null) { - // Updating the entry to be a bubble will trigger our normal update flow - setIsBubble(entry, isBubble, b.shouldAutoExpand()); - } else if (isBubble) { - // If bubble doesn't exist, it's a persisted bubble so we need to add it to the - // stack ourselves - Bubble bubble = mBubbleData.getOrCreateBubble(null, b /* persistedBubble */); - inflateAndAdd(bubble, bubble.shouldAutoExpand() /* suppressFlyout */, - !bubble.shouldAutoExpand() /* showInShade */); - } + mSysuiProxy.getPendingOrActiveEntry(b.getKey(), (entry) -> { + mMainExecutor.execute(() -> { + if (entry != null) { + // Updating the entry to be a bubble will trigger our normal update flow + setIsBubble(entry, isBubble, b.shouldAutoExpand()); + } else if (isBubble) { + // If bubble doesn't exist, it's a persisted bubble so we need to add it to the + // stack ourselves + Bubble bubble = mBubbleData.getOrCreateBubble(null, b /* persistedBubble */); + inflateAndAdd(bubble, bubble.shouldAutoExpand() /* suppressFlyout */, + !bubble.shouldAutoExpand() /* showInShade */); + } + }); + }); } @SuppressWarnings("FieldCanBeLocal") @@ -994,14 +1003,17 @@ public class BubbleController { } } - final BubbleEntry entry = mSysuiProxy.getPendingOrActiveEntry(bubble.getKey()); - if (entry != null) { - final String groupKey = entry.getStatusBarNotification().getGroupKey(); - if (getBubblesInGroup(groupKey).isEmpty()) { - // Time to potentially remove the summary - mSysuiProxy.notifyMaybeCancelSummary(bubble.getKey()); - } - } + mSysuiProxy.getPendingOrActiveEntry(bubble.getKey(), (entry) -> { + mMainExecutor.execute(() -> { + if (entry != null) { + final String groupKey = entry.getStatusBarNotification().getGroupKey(); + if (getBubblesInGroup(groupKey).isEmpty()) { + // Time to potentially remove the summary + mSysuiProxy.notifyMaybeCancelSummary(bubble.getKey()); + } + } + }); + }); } mDataRepository.removeBubbles(mCurrentUserId, bubblesToBeRemovedFromRepository); @@ -1064,8 +1076,8 @@ public class BubbleController { private boolean isSummaryOfBubbles(BubbleEntry entry) { String groupKey = entry.getStatusBarNotification().getGroupKey(); ArrayList<Bubble> bubbleChildren = getBubblesInGroup(groupKey); - boolean isSuppressedSummary = (mBubbleData.isSummarySuppressed(groupKey) - && mBubbleData.getSummaryKey(groupKey).equals(entry.getKey())); + boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey) + && mBubbleData.getSummaryKey(groupKey).equals(entry.getKey()); boolean isSummary = entry.getStatusBarNotification().getNotification().isGroupSummary(); return (isSuppressedSummary || isSummary) && !bubbleChildren.isEmpty(); } @@ -1123,23 +1135,6 @@ public class BubbleController { mStackView.updateContentDescription(); } - /** - * The task id of the expanded view, if the stack is expanded and not occluded by the - * status bar, otherwise returns {@link ActivityTaskManager#INVALID_TASK_ID}. - */ - private int getExpandedTaskId() { - if (mStackView == null) { - return INVALID_TASK_ID; - } - final BubbleViewProvider expandedViewProvider = mStackView.getExpandedBubble(); - if (expandedViewProvider != null && isStackExpanded() - && !mStackView.isExpansionAnimating() - && !mSysuiProxy.isNotificationShadeExpand()) { - return expandedViewProvider.getTaskId(); - } - return INVALID_TASK_ID; - } - @VisibleForTesting public BubbleStackView getStackView() { return mStackView; @@ -1345,9 +1340,10 @@ public class BubbleController { } @Override - public void onRankingUpdated(RankingMap rankingMap) { + public void onRankingUpdated(RankingMap rankingMap, + HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) { mMainExecutor.execute(() -> { - BubbleController.this.onRankingUpdated(rankingMap); + BubbleController.this.onRankingUpdated(rankingMap, entryDataByKey); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 9d196ba32f8a..53b75373a647 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -542,7 +542,8 @@ public class BubbleData { void overflowBubble(@DismissReason int reason, Bubble bubble) { if (bubble.getPendingIntentCanceled() || !(reason == Bubbles.DISMISS_AGED - || reason == Bubbles.DISMISS_USER_GESTURE)) { + || reason == Bubbles.DISMISS_USER_GESTURE + || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) { return; } if (DEBUG_BUBBLE_DATA) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 29458ef70e2d..2f31acd41408 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -85,6 +85,19 @@ public class BubbleExpandedView extends LinearLayout { private boolean mImeVisible; private boolean mNeedsNewHeight; + /** + * Whether we want the TaskView's content to be visible (alpha = 1f). If + * {@link #mIsAlphaAnimating} is true, this may not reflect the TaskView's actual alpha value + * until the animation ends. + */ + private boolean mIsContentVisible = false; + + /** + * Whether we're animating the TaskView's alpha value. If so, we will hold off on applying alpha + * changes from {@link #setContentVisibility} until the animation ends. + */ + private boolean mIsAlphaAnimating = false; + private int mMinHeight; private int mOverflowHeight; private int mSettingsIconHeight; @@ -150,6 +163,7 @@ public class BubbleExpandedView extends LinearLayout { // Apply flags to make behaviour match documentLaunchMode=always. fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + fillInIntent.putExtra(Intent.EXTRA_IS_BUBBLED, true); if (mBubble != null) { mBubble.setIntentActive(); } @@ -465,6 +479,29 @@ public class BubbleExpandedView extends LinearLayout { } /** + * Whether we are currently animating the TaskView's alpha value. If this is set to true, calls + * to {@link #setContentVisibility} will not be applied until this is set to false again. + */ + void setAlphaAnimating(boolean animating) { + mIsAlphaAnimating = animating; + + // If we're done animating, apply the correct + if (!animating) { + setContentVisibility(mIsContentVisible); + } + } + + /** + * Sets the alpha of the underlying TaskView, since changing the expanded view's alpha does not + * affect the TaskView since it uses a Surface. + */ + void setTaskViewAlpha(float alpha) { + if (mTaskView != null) { + mTaskView.setAlpha(alpha); + } + } + + /** * Set visibility of contents in the expanded state. * * @param visibility {@code true} if the contents should be visible on the screen. @@ -477,16 +514,19 @@ public class BubbleExpandedView extends LinearLayout { Log.d(TAG, "setContentVisibility: visibility=" + visibility + " bubble=" + getBubbleKey()); } + mIsContentVisible = visibility; + final float alpha = visibility ? 1f : 0f; mPointerView.setAlpha(alpha); - if (mTaskView != null) { + if (mTaskView != null && !mIsAlphaAnimating) { mTaskView.setAlpha(alpha); } } + @Nullable - View getTaskView() { + TaskView getTaskView() { return mTaskView; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java index 3361c4ce11da..c88a58be1461 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java @@ -61,7 +61,10 @@ public class BubbleLogger { BUBBLE_OVERFLOW_REMOVE_BLOCKED(490), @UiEvent(doc = "User selected the overflow.") - BUBBLE_OVERFLOW_SELECTED(600); + BUBBLE_OVERFLOW_SELECTED(600), + + @UiEvent(doc = "Restore bubble to overflow after phone reboot.") + BUBBLE_OVERFLOW_RECOVER(691); private final int mId; @@ -112,6 +115,8 @@ public class BubbleLogger { log(b, Event.BUBBLE_OVERFLOW_ADD_AGED); } else if (r == Bubbles.DISMISS_USER_GESTURE) { log(b, Event.BUBBLE_OVERFLOW_ADD_USER_GESTURE); + } else if (r == Bubbles.DISMISS_RELOAD_FROM_DISK) { + log(b, Event.BUBBLE_OVERFLOW_RECOVER); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt index 16cd3cf3686c..51d63cff385a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt @@ -167,8 +167,12 @@ class BubbleOverflow( return dotPath } - override fun setContentVisibility(visible: Boolean) { - expandedView?.setContentVisibility(visible) + override fun setExpandedContentAlpha(alpha: Float) { + expandedView?.alpha = alpha + } + + override fun setTaskViewVisibility(visible: Boolean) { + // Overflow does not have a TaskView. } override fun getIconView(): BadgedImageView? { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index af421facd72a..a3edc20e242a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -43,7 +43,6 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.os.Bundle; -import android.os.Handler; import android.provider.Settings; import android.util.Log; import android.view.Choreographer; @@ -123,6 +122,10 @@ public class BubbleStackView extends FrameLayout @VisibleForTesting static final int FLYOUT_HIDE_AFTER = 5000; + private static final float EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT = 0.1f; + + private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150; + /** * How long to wait to animate the stack temporarily invisible after a drag/flyout hide * animation ends, if we are in fact temporarily invisible. @@ -142,7 +145,7 @@ public class BubbleStackView extends FrameLayout private final PhysicsAnimator.SpringConfig mTranslateSpringConfig = new PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY); + SpringForce.STIFFNESS_VERY_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY); /** * Handler to use for all delayed animations - this way, we can easily cancel them before @@ -211,6 +214,9 @@ public class BubbleStackView extends FrameLayout /** Container for the animating-out SurfaceView. */ private FrameLayout mAnimatingOutSurfaceContainer; + /** Animator for animating the alpha value of the animating out SurfaceView. */ + private final ValueAnimator mAnimatingOutSurfaceAlphaAnimator = ValueAnimator.ofFloat(0f, 1f); + /** * Buffer containing a screenshot of the animating-out bubble. This is drawn into the * SurfaceView during animations. @@ -261,6 +267,12 @@ public class BubbleStackView extends FrameLayout /** Whether we're in the middle of dragging the stack around by touch. */ private boolean mIsDraggingStack = false; + /** Whether the expanded view has been hidden, because we are dragging out a bubble. */ + private boolean mExpandedViewHidden = false; + + /** Animator for animating the expanded view's alpha (including the TaskView inside it). */ + private final ValueAnimator mExpandedViewAlphaAnimator = ValueAnimator.ofFloat(0f, 1f); + /** * The pointer index of the ACTION_DOWN event we received prior to an ACTION_UP. We'll ignore * touches from other pointer indices. @@ -614,6 +626,12 @@ public class BubbleStackView extends FrameLayout // Show the dismiss target, if we haven't already. mDismissView.show(); + if (mIsExpanded && mExpandedBubble != null && v.equals(mExpandedBubble.getIconView())) { + // Hide the expanded view if we're dragging out the expanded bubble, and we haven't + // already hidden it. + hideExpandedViewIfNeeded(); + } + // First, see if the magnetized object consumes the event - if so, we shouldn't move the // bubble since it's stuck to the target. if (!passEventToMagnetizedObject(ev)) { @@ -645,6 +663,9 @@ public class BubbleStackView extends FrameLayout if (!passEventToMagnetizedObject(ev)) { if (mBubbleData.isExpanded()) { mExpandedAnimationController.snapBubbleBack(v, velX, velY); + + // Re-show the expanded view if we hid it. + showExpandedViewIfNeeded(); } else { // Fling the stack to the edge, and save whether or not it's going to end up on // the left side of the screen. @@ -937,6 +958,46 @@ public class BubbleStackView extends FrameLayout animate() .setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED) .setDuration(FADE_IN_DURATION); + + mExpandedViewAlphaAnimator.setDuration(EXPANDED_VIEW_ALPHA_ANIMATION_DURATION); + mExpandedViewAlphaAnimator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED); + mExpandedViewAlphaAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + // We need to be Z ordered on top in order for alpha animations to work. + mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true); + mExpandedBubble.getExpandedView().setAlphaAnimating(true); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false); + mExpandedBubble.getExpandedView().setAlphaAnimating(false); + } + } + }); + mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> { + if (mExpandedBubble != null) { + mExpandedBubble.setExpandedContentAlpha((float) valueAnimator.getAnimatedValue()); + } + }); + + mAnimatingOutSurfaceAlphaAnimator.setDuration(EXPANDED_VIEW_ALPHA_ANIMATION_DURATION); + mAnimatingOutSurfaceAlphaAnimator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED); + mAnimatingOutSurfaceAlphaAnimator.addUpdateListener(valueAnimator -> { + if (!mExpandedViewHidden) { + mAnimatingOutSurfaceView.setAlpha((float) valueAnimator.getAnimatedValue()); + } + }); + mAnimatingOutSurfaceAlphaAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + releaseAnimatingOutBubbleBuffer(); + } + }); } /** @@ -1478,6 +1539,9 @@ public class BubbleStackView extends FrameLayout * Update bubble order and pointer position. */ public void updateBubbleOrder(List<Bubble> bubbles) { + if (isExpansionAnimating()) { + return; + } final Runnable reorder = () -> { for (int i = 0; i < bubbles.size(); i++) { Bubble bubble = bubbles.get(i); @@ -1536,7 +1600,8 @@ public class BubbleStackView extends FrameLayout // If we're expanded, screenshot the currently expanded bubble (before expanding the newly // selected bubble) so we can animate it out. - if (mIsExpanded && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + if (mIsExpanded && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null + && !mExpandedViewHidden) { if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { // Before screenshotting, have the real ActivityView show on top of other surfaces // so that the screenshot doesn't flicker on top of it. @@ -1572,7 +1637,7 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainer.setAlpha(0.0f); mSurfaceSynchronizer.syncSurfaceAndRun(() -> { if (previouslySelected != null) { - previouslySelected.setContentVisibility(false); + previouslySelected.setTaskViewVisibility(false); } updateExpandedBubble(); @@ -1653,6 +1718,58 @@ public class BubbleStackView extends FrameLayout requestUpdate(); } + /** Animate the expanded view hidden. This is done while we're dragging out a bubble. */ + private void hideExpandedViewIfNeeded() { + if (mExpandedViewHidden + || mExpandedBubble == null + || mExpandedBubble.getExpandedView() == null) { + return; + } + + mExpandedViewHidden = true; + + // Scale down. + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) + .spring(AnimatableScaleMatrix.SCALE_X, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor( + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT), + mScaleOutSpringConfig) + .spring(AnimatableScaleMatrix.SCALE_Y, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor( + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT), + mScaleOutSpringConfig) + .addUpdateListener((target, values) -> + mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix)) + .start(); + + // Animate alpha from 1f to 0f. + mExpandedViewAlphaAnimator.reverse(); + } + + /** + * Animate the expanded view visible again. This is done when we're done dragging out a bubble. + */ + private void showExpandedViewIfNeeded() { + if (!mExpandedViewHidden) { + return; + } + + mExpandedViewHidden = false; + + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) + .spring(AnimatableScaleMatrix.SCALE_X, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), + mScaleOutSpringConfig) + .spring(AnimatableScaleMatrix.SCALE_Y, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), + mScaleOutSpringConfig) + .addUpdateListener((target, values) -> + mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix)) + .start(); + + mExpandedViewAlphaAnimator.start(); + } + private void animateExpansion() { cancelDelayedExpandCollapseSwitchAnimations(); final boolean showVertically = mPositioner.showBubblesVertically(); @@ -1662,6 +1779,7 @@ public class BubbleStackView extends FrameLayout } beforeExpandedViewAnimation(); + updateBadgesAndZOrder(false /* setBadgeForCollapsedStack */); mBubbleContainer.setActiveController(mExpandedAnimationController); updateOverflowVisibility(); updatePointerPosition(); @@ -1710,7 +1828,7 @@ public class BubbleStackView extends FrameLayout // Should not happen since we lay out before expanding, but just in case... if (getWidth() > 0) { startDelay = (long) - (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION + (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION * 1.2f + (distanceAnimated / getWidth()) * 30); } @@ -1724,20 +1842,29 @@ public class BubbleStackView extends FrameLayout pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding; } mExpandedViewContainerMatrix.setScale( - 0f, 0f, + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, pivotX, pivotY); } else { mExpandedViewContainerMatrix.setScale( - 0f, 0f, - bubbleWillBeAt + mBubbleSize / 2f, getExpandedViewY()); + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, + bubbleWillBeAt + mBubbleSize / 2f, + getExpandedViewY()); } mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false); + if (mExpandedBubble.getExpandedView() != null) { + mExpandedBubble.setExpandedContentAlpha(0f); + + // We'll be starting the alpha animation after a slight delay, so set this flag early + // here. + mExpandedBubble.getExpandedView().setAlphaAnimating(true); } mDelayedAnimationExecutor.executeDelayed(() -> { + mExpandedViewAlphaAnimator.start(); + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) .spring(AnimatableScaleMatrix.SCALE_X, @@ -1792,22 +1919,15 @@ public class BubbleStackView extends FrameLayout // since we're about to animate collapsed. mExpandedAnimationController.notifyPreparingToCollapse(); - final long startDelay = - (long) (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION * 0.6f); - mDelayedAnimationExecutor.executeDelayed(() -> { - mExpandedAnimationController.collapseBackToStack( - mStackAnimationController.getStackPositionAlongNearestHorizontalEdge() - /* collapseTo */, - () -> mBubbleContainer.setActiveController(mStackAnimationController)); - }, startDelay); + mExpandedAnimationController.collapseBackToStack( + mStackAnimationController.getStackPositionAlongNearestHorizontalEdge() + /* collapseTo */, + () -> mBubbleContainer.setActiveController(mStackAnimationController)); if (mTaskbarScrim.getVisibility() == VISIBLE) { mTaskbarScrim.animate().alpha(0f).start(); } - // We want to visually collapse into this bubble during the animation. - final View expandingFromBubble = mExpandedBubble.getIconView(); - int index; if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) { index = mBubbleData.getBubbles().size(); @@ -1836,31 +1956,25 @@ public class BubbleStackView extends FrameLayout getExpandedViewY()); } + mExpandedViewAlphaAnimator.reverse(); + + // When the animation completes, we should no longer be showing the content. + if (mExpandedBubble.getExpandedView() != null) { + mExpandedBubble.getExpandedView().setContentVisibility(false); + } + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) - .spring(AnimatableScaleMatrix.SCALE_X, 0f, mScaleOutSpringConfig) - .spring(AnimatableScaleMatrix.SCALE_Y, 0f, mScaleOutSpringConfig) + .spring(AnimatableScaleMatrix.SCALE_X, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor( + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT), + mScaleOutSpringConfig) + .spring(AnimatableScaleMatrix.SCALE_Y, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor( + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT), + mScaleOutSpringConfig) .addUpdateListener((target, values) -> { - if (expandingFromBubble != null) { - // Follow the bubble as it translates! - if (showVertically) { - mExpandedViewContainerMatrix.postTranslate( - 0f, expandingFromBubble.getTranslationY() - - expandingFromBubbleAt); - } else { - mExpandedViewContainerMatrix.postTranslate( - expandingFromBubble.getTranslationX() - - expandingFromBubbleAt, 0f); - } - } - mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); - - // Hide early so we don't have a tiny little expanded view still visible at the - // end of the scale animation. - if (mExpandedViewContainerMatrix.getScaleX() < 0.05f) { - mExpandedViewContainer.setVisibility(View.INVISIBLE); - } }) .withEndActions(() -> { final BubbleViewProvider previouslySelected = mExpandedBubble; @@ -1875,10 +1989,10 @@ public class BubbleStackView extends FrameLayout mExpandedBubble)); } updateOverflowVisibility(); - + updateBadgesAndZOrder(true /* setBadgeForCollapsedStack */); afterExpandedViewAnimation(); if (previouslySelected != null) { - previouslySelected.setContentVisibility(false); + previouslySelected.setTaskViewVisibility(false); } if (mPositioner.showingInTaskbar()) { @@ -1899,22 +2013,21 @@ public class BubbleStackView extends FrameLayout // The surface contains a screenshot of the animating out bubble, so we just need to animate // it out (and then release the GraphicBuffer). PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); - PhysicsAnimator animator = PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer) - .spring(DynamicAnimation.SCALE_X, 0f, mScaleOutSpringConfig) - .spring(DynamicAnimation.SCALE_Y, 0f, mScaleOutSpringConfig) - .withEndActions(this::releaseAnimatingOutBubbleBuffer); + + mAnimatingOutSurfaceAlphaAnimator.reverse(); + mExpandedViewAlphaAnimator.start(); if (mPositioner.showBubblesVertically()) { float translationX = mStackAnimationController.isStackOnLeftSide() ? mAnimatingOutSurfaceContainer.getTranslationX() + mBubbleSize * 2 : mAnimatingOutSurfaceContainer.getTranslationX(); - animator.spring(DynamicAnimation.TRANSLATION_X, - translationX, - mTranslateSpringConfig) + PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer) + .spring(DynamicAnimation.TRANSLATION_X, translationX, mTranslateSpringConfig) .start(); } else { - animator.spring(DynamicAnimation.TRANSLATION_Y, - mAnimatingOutSurfaceContainer.getTranslationY() - mBubbleSize * 2, + PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer) + .spring(DynamicAnimation.TRANSLATION_Y, + mAnimatingOutSurfaceContainer.getTranslationY() - mBubbleSize, mTranslateSpringConfig) .start(); } @@ -1943,7 +2056,8 @@ public class BubbleStackView extends FrameLayout pivotX, pivotY); } else { mExpandedViewContainerMatrix.setScale( - 0f, 0f, + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, expandingFromBubbleDestination + mBubbleSize / 2f, getExpandedViewY()); } @@ -1968,10 +2082,7 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); }) .withEndActions(() -> { - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false); - } - + mExpandedViewHidden = false; mIsBubbleSwitchAnimating = false; }) .start(); @@ -2485,6 +2596,7 @@ public class BubbleStackView extends FrameLayout && mExpandedBubble.getExpandedView() != null) { BubbleExpandedView bev = mExpandedBubble.getExpandedView(); bev.setContentVisibility(false); + bev.setAlphaAnimating(!mIsExpansionAnimating); mExpandedViewContainerMatrix.setScaleX(0f); mExpandedViewContainerMatrix.setScaleY(0f); mExpandedViewContainerMatrix.setTranslate(0f, 0f); @@ -2582,7 +2694,14 @@ public class BubbleStackView extends FrameLayout mAnimatingOutBubbleBuffer.getHardwareBuffer(), mAnimatingOutBubbleBuffer.getColorSpace()); - mSurfaceSynchronizer.syncSurfaceAndRun(() -> post(() -> onComplete.accept(true))); + mAnimatingOutSurfaceView.setAlpha(1f); + mExpandedViewContainer.setVisibility(View.GONE); + + mSurfaceSynchronizer.syncSurfaceAndRun(() -> { + post(() -> { + onComplete.accept(true); + }); + }); }); } @@ -2614,7 +2733,9 @@ public class BubbleStackView extends FrameLayout } } mExpandedViewContainer.setPadding(leftPadding, 0, rightPadding, 0); - mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); + if (mIsExpansionAnimating) { + mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); + } if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { mExpandedViewContainer.setTranslationY(getExpandedViewY()); mExpandedViewContainer.setTranslationX(0f); @@ -2623,7 +2744,6 @@ public class BubbleStackView extends FrameLayout } mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); - updateBadgesAndZOrder(false /* setBadgeForCollapsedStack */); } /** @@ -2733,9 +2853,13 @@ public class BubbleStackView extends FrameLayout * @param action the user interaction enum. */ private void logBubbleEvent(@Nullable BubbleViewProvider provider, int action) { + final String packageName = + (provider != null && provider instanceof Bubble) + ? ((Bubble) provider).getPackageName() + : "null"; mBubbleData.logBubbleEvent(provider, action, - mContext.getApplicationInfo().packageName, + packageName, getBubbleCount(), getBubbleIndex(provider), getNormalizedXPosition(), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java index ec900be13658..da4259c42558 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java @@ -29,7 +29,16 @@ import androidx.annotation.Nullable; public interface BubbleViewProvider { @Nullable BubbleExpandedView getExpandedView(); - void setContentVisibility(boolean visible); + /** + * Sets the alpha of the expanded view content. This will be applied to both the expanded view + * container itself (the manage button, etc.) as well as the TaskView within it. + */ + void setExpandedContentAlpha(float alpha); + + /** + * Sets whether the contents of the bubble's TaskView should be visible. + */ + void setTaskViewVisibility(boolean visible); @Nullable View getIconView(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 6102147e4b53..8e061e9c9874 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -26,6 +26,7 @@ import android.os.Bundle; import android.os.Looper; import android.service.notification.NotificationListenerService.RankingMap; import android.util.ArraySet; +import android.util.Pair; import android.view.View; import androidx.annotation.IntDef; @@ -37,6 +38,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import java.util.HashMap; import java.util.List; import java.util.concurrent.Executor; import java.util.function.BiConsumer; @@ -54,7 +56,7 @@ public interface Bubbles { DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT, DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED, - DISMISS_NO_BUBBLE_UP}) + DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK}) @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @interface DismissReason {} @@ -72,6 +74,7 @@ public interface Bubbles { int DISMISS_SHORTCUT_REMOVED = 12; int DISMISS_PACKAGE_REMOVED = 13; int DISMISS_NO_BUBBLE_UP = 14; + int DISMISS_RELOAD_FROM_DISK = 15; /** * @return {@code true} if there is a bubble associated with the provided key and if its @@ -181,8 +184,11 @@ public interface Bubbles { * permissions on the notification channel or the global setting. * * @param rankingMap the updated ranking map from NotificationListenerService + * @param entryDataByKey a map of ranking key to bubble entry and whether the entry should + * bubble up */ - void onRankingUpdated(RankingMap rankingMap); + void onRankingUpdated(RankingMap rankingMap, + HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey); /** * Called when the status bar has become visible or invisible (either permanently or @@ -242,14 +248,10 @@ public interface Bubbles { /** Callback to tell SysUi components execute some methods. */ interface SysuiProxy { - @Nullable - BubbleEntry getPendingOrActiveEntry(String key); + void getPendingOrActiveEntry(String key, Consumer<BubbleEntry> callback); - List<BubbleEntry> getShouldRestoredEntries(ArraySet<String> savedBubbleKeys); - - boolean isNotificationShadeExpand(); - - boolean shouldBubbleUp(String key); + void getShouldRestoredEntries(ArraySet<String> savedBubbleKeys, + Consumer<List<BubbleEntry>> callback); void setNotificationInterruption(String key); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index e1f831eae699..73371e7eff20 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -62,8 +62,8 @@ public class StackAnimationController extends private static final String TAG = "Bubbs.StackCtrl"; - /** Values to use for animating bubbles in. */ - private static final float ANIMATE_IN_STIFFNESS = 1000f; + /** Value to use for animating bubbles in and springing stack after fling. */ + private static final float STACK_SPRING_STIFFNESS = 700f; /** Values to use for animating updated bubble to top of stack. */ private static final float NEW_BUBBLE_START_SCALE = 0.5f; @@ -80,20 +80,15 @@ public class StackAnimationController extends private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfig = new PhysicsAnimator.SpringConfig( - ANIMATE_IN_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY); + STACK_SPRING_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY); /** * Friction applied to fling animations. Since the stack must land on one of the sides of the * screen, we want less friction horizontally so that the stack has a better chance of making it * to the side without needing a spring. */ - private static final float FLING_FRICTION = 2.2f; + private static final float FLING_FRICTION = 1.9f; - /** - * Values to use for the stack spring animation used to spring the stack to its final position - * after a fling. - */ - private static final int SPRING_AFTER_FLING_STIFFNESS = 750; private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f; /** Sentinel value for unset position value. */ @@ -216,7 +211,7 @@ public class StackAnimationController extends @Override public void moveToBounds(@NonNull Rect bounds) { - springStack(bounds.left, bounds.top, SpringForce.STIFFNESS_LOW); + springStack(bounds.left, bounds.top, STACK_SPRING_STIFFNESS); } @NonNull @@ -341,7 +336,7 @@ public class StackAnimationController extends * flings. */ public void springStackAfterFling(float destinationX, float destinationY) { - springStack(destinationX, destinationY, SPRING_AFTER_FLING_STIFFNESS); + springStack(destinationX, destinationY, STACK_SPRING_STIFFNESS); } /** @@ -371,7 +366,7 @@ public class StackAnimationController extends final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); final float stiffness = Settings.Secure.getFloat(contentResolver, "bubble_stiffness", - SPRING_AFTER_FLING_STIFFNESS /* default */); + STACK_SPRING_STIFFNESS /* default */); final float dampingRatio = Settings.Secure.getFloat(contentResolver, "bubble_damping", SPRING_AFTER_FLING_DAMPING_RATIO); final float friction = Settings.Secure.getFloat(contentResolver, "bubble_friction", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index a2cd683f2fdd..b2ac61cf3f6e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -159,6 +159,14 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } + private void dispatchVisibilityChanged(int displayId, boolean isShowing) { + synchronized (mPositionProcessors) { + for (ImePositionProcessor pp : mPositionProcessors) { + pp.onImeVisibilityChanged(displayId, isShowing); + } + } + } + /** * Adds an {@link ImePositionProcessor} to be called during ime position updates. */ @@ -212,7 +220,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged return; } - mImeShowing = insetsState.getSourceOrDefaultVisibility(InsetsState.ITYPE_IME); + updateImeVisibility(insetsState.getSourceOrDefaultVisibility(InsetsState.ITYPE_IME)); final InsetsSource newSource = insetsState.getSource(InsetsState.ITYPE_IME); final Rect newFrame = newSource.getFrame(); @@ -371,7 +379,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged seek = true; } mAnimationDirection = show ? DIRECTION_SHOW : DIRECTION_HIDE; - mImeShowing = show; + updateImeVisibility(show); mAnimation = ValueAnimator.ofFloat(startY, endY); mAnimation.setDuration( show ? ANIMATION_DURATION_SHOW_MS : ANIMATION_DURATION_HIDE_MS); @@ -455,6 +463,13 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } + private void updateImeVisibility(boolean isShowing) { + if (mImeShowing != isShowing) { + mImeShowing = isShowing; + dispatchVisibilityChanged(mDisplayId, isShowing); + } + } + @VisibleForTesting @BinderThread public class DisplayWindowInsetsControllerImpl @@ -563,6 +578,15 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged default void onImeEndPositioning(int displayId, boolean cancel, SurfaceControl.Transaction t) { } + + /** + * Called when the IME visibility changed. + * + * @param isShowing {@code true} if the IME is shown. + */ + default void onImeVisibilityChanged(int displayId, boolean isShowing) { + + } } public IInputMethodManager getImms() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java index 616f24a874bc..fb70cbe502b0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java @@ -40,9 +40,11 @@ import android.view.IWindowSessionCallback; import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.SurfaceControl; +import android.view.SurfaceSession; import android.view.SurfaceControlViewHost; import android.view.View; import android.view.ViewGroup; +import android.view.ViewRootImpl; import android.view.WindowManager; import android.view.WindowlessWindowManager; import android.window.ClientWindowFrames; @@ -251,6 +253,8 @@ public class SystemWindows { public class SysUiWindowManager extends WindowlessWindowManager { final int mDisplayId; ContainerWindow mContainerWindow; + final HashMap<IBinder, SurfaceControl> mLeashForWindow = + new HashMap<IBinder, SurfaceControl>(); public SysUiWindowManager(int displayId, Context ctx, SurfaceControl rootSurface, ContainerWindow container) { super(ctx.getResources().getConfiguration(), rootSurface, null /* hostInputToken */); @@ -263,7 +267,33 @@ public class SystemWindows { } SurfaceControl getSurfaceControlForWindow(View rootView) { - return getSurfaceControl(rootView); + synchronized (this) { + return mLeashForWindow.get(getWindowBinder(rootView)); + } + } + + protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + SurfaceControl leash = new SurfaceControl.Builder(new SurfaceSession()) + .setContainerLayer() + .setName("SystemWindowLeash") + .setHidden(false) + .setParent(mRootSurface) + .setCallsite("SysUiWIndowManager#attachToParentSurface").build(); + synchronized (this) { + mLeashForWindow.put(window.asBinder(), leash); + } + b.setParent(leash); + } + + @Override + public void remove(android.view.IWindow window) throws RemoteException { + super.remove(window); + synchronized(this) { + IBinder token = window.asBinder(); + new SurfaceControl.Transaction().remove(mLeashForWindow.get(token)) + .apply(); + mLeashForWindow.remove(token); + } } void setTouchableRegionForWindow(View rootView, Region region) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java index 00bd9e507335..59374a6069c8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java @@ -19,7 +19,6 @@ package com.android.wm.shell.common; import android.app.ActivityManager.RunningTaskInfo; import android.app.ITaskStackListener; import android.content.ComponentName; -import android.os.IBinder; import android.window.TaskSnapshot; import androidx.annotation.BinderThread; @@ -85,6 +84,4 @@ public interface TaskStackListenerCallback { default void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) { } default void onActivityRotation(int displayId) { } - - default void onSizeCompatModeActivityChanged(int displayId, IBinder activityToken) { } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java index db34248d491b..e94080aa8db7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java @@ -22,7 +22,6 @@ import android.app.IActivityTaskManager; import android.app.TaskStackListener; import android.content.ComponentName; import android.os.Handler; -import android.os.IBinder; import android.os.Message; import android.os.Trace; import android.util.Log; @@ -54,13 +53,12 @@ public class TaskStackListenerImpl extends TaskStackListener implements Handler. private static final int ON_TASK_MOVED_TO_FRONT = 12; private static final int ON_ACTIVITY_REQUESTED_ORIENTATION_CHANGE = 13; private static final int ON_ACTIVITY_LAUNCH_ON_SECONDARY_DISPLAY_REROUTED = 14; - private static final int ON_SIZE_COMPAT_MODE_ACTIVITY_CHANGED = 15; - private static final int ON_BACK_PRESSED_ON_TASK_ROOT = 16; - private static final int ON_TASK_DISPLAY_CHANGED = 17; - private static final int ON_TASK_LIST_UPDATED = 18; - private static final int ON_TASK_LIST_FROZEN_UNFROZEN = 19; - private static final int ON_TASK_DESCRIPTION_CHANGED = 20; - private static final int ON_ACTIVITY_ROTATION = 21; + private static final int ON_BACK_PRESSED_ON_TASK_ROOT = 15; + private static final int ON_TASK_DISPLAY_CHANGED = 16; + private static final int ON_TASK_LIST_UPDATED = 17; + private static final int ON_TASK_LIST_FROZEN_UNFROZEN = 18; + private static final int ON_TASK_DESCRIPTION_CHANGED = 19; + private static final int ON_ACTIVITY_ROTATION = 20; /** * List of {@link TaskStackListenerCallback} registered from {@link #addListener}. @@ -264,13 +262,6 @@ public class TaskStackListenerImpl extends TaskStackListener implements Handler. } @Override - public void onSizeCompatModeActivityChanged(int displayId, IBinder activityToken) { - mMainHandler.obtainMessage(ON_SIZE_COMPAT_MODE_ACTIVITY_CHANGED, displayId, - 0 /* unused */, - activityToken).sendToTarget(); - } - - @Override public boolean handleMessage(Message msg) { synchronized (mTaskStackListeners) { switch (msg.what) { @@ -383,13 +374,6 @@ public class TaskStackListenerImpl extends TaskStackListener implements Handler. } break; } - case ON_SIZE_COMPAT_MODE_ACTIVITY_CHANGED: { - for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { - mTaskStackListeners.get(i).onSizeCompatModeActivityChanged( - msg.arg1, (IBinder) msg.obj); - } - break; - } case ON_BACK_PRESSED_ON_TASK_ROOT: { for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { mTaskStackListeners.get(i).onBackPressedOnTaskRoot( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java index 7f9c34f5df7a..87f0c25c93df 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java @@ -39,6 +39,7 @@ import android.view.IWindow; import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; +import android.view.SurfaceSession; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -55,6 +56,7 @@ public final class SplitWindowManager extends WindowlessWindowManager { private final ParentContainerCallbacks mParentContainerCallbacks; private Context mContext; private SurfaceControlViewHost mViewHost; + private SurfaceControl mLeash; private boolean mResizingSplits; private final String mWindowName; @@ -87,8 +89,16 @@ public final class SplitWindowManager extends WindowlessWindowManager { } @Override - protected void attachToParentSurface(SurfaceControl.Builder b) { - mParentContainerCallbacks.attachToParentSurface(b); + protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later. + final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + .setContainerLayer() + .setName(TAG) + .setHidden(false) + .setCallsite("SplitWindowManager#attachToParentSurface"); + mParentContainerCallbacks.attachToParentSurface(builder); + mLeash = builder.build(); + b.setParent(mLeash); } /** Inflates {@link DividerView} on to the root surface. */ @@ -118,9 +128,15 @@ public final class SplitWindowManager extends WindowlessWindowManager { * hierarchy. */ void release() { - if (mViewHost == null) return; - mViewHost.release(); - mViewHost = null; + if (mViewHost != null){ + mViewHost.release(); + mViewHost = null; + } + + if (mLeash != null) { + new SurfaceControl.Transaction().remove(mLeash).apply(); + mLeash = null; + } } void setResizingSplits(boolean resizing) { @@ -139,6 +155,6 @@ public final class SplitWindowManager extends WindowlessWindowManager { */ @Nullable SurfaceControl getSurfaceControl() { - return mViewHost == null ? null : getSurfaceControl(mViewHost.getWindowToken()); + return mLeash; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index c8938ad40aba..17709438baba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -53,6 +53,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreen; +import com.android.wm.shell.splitscreen.SplitScreenController; import java.util.Optional; @@ -66,7 +67,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange private final Context mContext; private final DisplayController mDisplayController; - private SplitScreen mSplitScreen; + private SplitScreenController mSplitScreen; private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>(); private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); @@ -76,7 +77,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange mDisplayController = displayController; } - public void initialize(Optional<SplitScreen> splitscreen) { + public void initialize(Optional<SplitScreenController> splitscreen) { mSplitScreen = splitscreen.orElse(null); mDisplayController.addDisplayWindowListener(this); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index 800150c0a93c..6f5f2eb5723c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -34,8 +34,11 @@ import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPL import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; -import static com.android.wm.shell.splitscreen.SplitScreen.SIDE_STAGE_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.splitscreen.SplitScreen.SIDE_STAGE_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import android.app.ActivityManager; import android.app.ActivityTaskManager; @@ -61,7 +64,9 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.splitscreen.SplitScreen; +import com.android.wm.shell.splitscreen.SplitScreen.StagePosition; +import com.android.wm.shell.splitscreen.SplitScreen.StageType; +import com.android.wm.shell.splitscreen.SplitScreenController; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -78,23 +83,22 @@ public class DragAndDropPolicy { private final Context mContext; private final ActivityTaskManager mActivityTaskManager; private final Starter mStarter; - private final SplitScreen mSplitScreen; + private final SplitScreenController mSplitScreen; private final ArrayList<DragAndDropPolicy.Target> mTargets = new ArrayList<>(); private DragSession mSession; - public DragAndDropPolicy(Context context, SplitScreen splitScreen) { - this(context, ActivityTaskManager.getInstance(), splitScreen, - new DefaultStarter(context, splitScreen)); + public DragAndDropPolicy(Context context, SplitScreenController splitScreen) { + this(context, ActivityTaskManager.getInstance(), splitScreen, new DefaultStarter(context)); } @VisibleForTesting DragAndDropPolicy(Context context, ActivityTaskManager activityTaskManager, - SplitScreen splitScreen, Starter starter) { + SplitScreenController splitScreen, Starter starter) { mContext = context; mActivityTaskManager = activityTaskManager; mSplitScreen = splitScreen; - mStarter = starter; + mStarter = mSplitScreen != null ? mSplitScreen : starter; } /** @@ -195,38 +199,43 @@ public class DragAndDropPolicy { return; } - final ClipDescription description = data.getDescription(); - final boolean isTask = description.hasMimeType(MIMETYPE_APPLICATION_TASK); - final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT); - final Intent dragData = mSession.dragData; final boolean inSplitScreen = mSplitScreen != null && mSplitScreen.isSplitScreenVisible(); final boolean leftOrTop = target.type == TYPE_SPLIT_TOP || target.type == TYPE_SPLIT_LEFT; - final Bundle opts = dragData.hasExtra(EXTRA_ACTIVITY_OPTIONS) - ? dragData.getBundleExtra(EXTRA_ACTIVITY_OPTIONS) - : new Bundle(); - - if (target.type == TYPE_FULLSCREEN) { - // Exit split stages if needed - mStarter.exitSplitScreen(); - } else if (mSplitScreen != null) { + + @StageType int stage = STAGE_TYPE_UNDEFINED; + @StagePosition int position = STAGE_POSITION_UNDEFINED; + if (target.type != TYPE_FULLSCREEN && mSplitScreen != null) { // Update launch options for the split side we are targeting. - final int position = leftOrTop - ? SIDE_STAGE_POSITION_TOP_OR_LEFT : SIDE_STAGE_POSITION_BOTTOM_OR_RIGHT; + position = leftOrTop ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT; if (!inSplitScreen) { - // Update the side stage position to match where we want to launch. - mSplitScreen.setSideStagePosition(position); + // Launch in the side stage if we are not in split-screen already. + stage = STAGE_TYPE_SIDE; } - mSplitScreen.updateActivityOptions(opts, position); } + final ClipDescription description = data.getDescription(); + final Intent dragData = mSession.dragData; + startClipDescription(description, dragData, stage, position); + } + + private void startClipDescription(ClipDescription description, Intent intent, + @StageType int stage, @StagePosition int position) { + final boolean isTask = description.hasMimeType(MIMETYPE_APPLICATION_TASK); + final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT); + final Bundle opts = intent.hasExtra(EXTRA_ACTIVITY_OPTIONS) + ? intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS) : new Bundle(); + if (isTask) { - mStarter.startTask(dragData.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID), opts); + final int taskId = intent.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID); + mStarter.startTask(taskId, stage, position, opts); } else if (isShortcut) { - mStarter.startShortcut(dragData.getStringExtra(EXTRA_PACKAGE_NAME), - dragData.getStringExtra(EXTRA_SHORTCUT_ID), - opts, dragData.getParcelableExtra(EXTRA_USER)); + final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); + final String id = intent.getStringExtra(EXTRA_SHORTCUT_ID); + final UserHandle user = intent.getParcelableExtra(EXTRA_USER); + mStarter.startShortcut(packageName, id, stage, position, opts, user); } else { - mStarter.startIntent(dragData.getParcelableExtra(EXTRA_PENDING_INTENT), opts); + mStarter.startIntent(intent.getParcelableExtra(EXTRA_PENDING_INTENT), stage, position, + opts); } } @@ -247,7 +256,6 @@ public class DragAndDropPolicy { int runningTaskActType = ACTIVITY_TYPE_STANDARD; boolean runningTaskIsResizeable; boolean dragItemSupportsSplitscreen; - boolean isPhone; DragSession(Context context, ActivityTaskManager activityTaskManager, DisplayLayout dispLayout, ClipData data) { @@ -275,7 +283,6 @@ public class DragAndDropPolicy { final ActivityInfo info = mInitialDragData.getItemAt(0).getActivityInfo(); dragItemSupportsSplitscreen = info == null || ActivityInfo.isResizeableMode(info.resizeMode); - isPhone = mContext.getResources().getConfiguration().smallestScreenWidthDp < 600; dragData = mInitialDragData.getItemAt(0).getIntent(); } } @@ -283,12 +290,13 @@ public class DragAndDropPolicy { /** * Interface for actually committing the task launches. */ - @VisibleForTesting - interface Starter { - void startTask(int taskId, Bundle activityOptions); - void startShortcut(String packageName, String shortcutId, Bundle activityOptions, - UserHandle user); - void startIntent(PendingIntent intent, Bundle activityOptions); + public interface Starter { + void startTask(int taskId, @StageType int stage, @StagePosition int position, + @Nullable Bundle options); + void startShortcut(String packageName, String shortcutId, @StageType int stage, + @StagePosition int position, @Nullable Bundle options, UserHandle user); + void startIntent(PendingIntent intent, @StageType int stage, @StagePosition int position, + @Nullable Bundle options); void enterSplitScreen(int taskId, boolean leftOrTop); void exitSplitScreen(); } @@ -299,39 +307,39 @@ public class DragAndDropPolicy { */ private static class DefaultStarter implements Starter { private final Context mContext; - private final SplitScreen mSplitScreen; - public DefaultStarter(Context context, SplitScreen splitScreen) { + public DefaultStarter(Context context) { mContext = context; - mSplitScreen = splitScreen; } @Override - public void startTask(int taskId, Bundle activityOptions) { + public void startTask(int taskId, int stage, int position, + @Nullable Bundle options) { try { - ActivityTaskManager.getService().startActivityFromRecents(taskId, activityOptions); + ActivityTaskManager.getService().startActivityFromRecents(taskId, options); } catch (RemoteException e) { Slog.e(TAG, "Failed to launch task", e); } } @Override - public void startShortcut(String packageName, String shortcutId, Bundle activityOptions, - UserHandle user) { + public void startShortcut(String packageName, String shortcutId, int stage, int position, + @Nullable Bundle options, UserHandle user) { try { LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class); launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, - activityOptions, user); + options, user); } catch (ActivityNotFoundException e) { Slog.e(TAG, "Failed to launch shortcut", e); } } @Override - public void startIntent(PendingIntent intent, Bundle activityOptions) { + public void startIntent(PendingIntent intent, int stage, int position, + @Nullable Bundle options) { try { - intent.send(null, 0, null, null, null, null, activityOptions); + intent.send(null, 0, null, null, null, null, options); } catch (PendingIntent.CanceledException e) { Slog.e(TAG, "Failed to launch activity", e); } @@ -339,14 +347,12 @@ public class DragAndDropPolicy { @Override public void enterSplitScreen(int taskId, boolean leftOrTop) { - mSplitScreen.moveToSideStage(taskId, - leftOrTop ? SIDE_STAGE_POSITION_TOP_OR_LEFT - : SIDE_STAGE_POSITION_BOTTOM_OR_RIGHT); + throw new UnsupportedOperationException("enterSplitScreen not implemented by starter"); } @Override public void exitSplitScreen() { - mSplitScreen.exitSplitScreen(); + throw new UnsupportedOperationException("exitSplitScreen not implemented by starter"); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java index 82c4e440fb15..b3423362347f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java @@ -42,7 +42,7 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import com.android.wm.shell.splitscreen.SplitScreen; +import com.android.wm.shell.splitscreen.SplitScreenController; import java.util.ArrayList; @@ -61,7 +61,7 @@ public class DragLayout extends View { private boolean mIsShowing; private boolean mHasDropped; - public DragLayout(Context context, SplitScreen splitscreen) { + public DragLayout(Context context, SplitScreenController splitscreen) { super(context); mPolicy = new DragAndDropPolicy(context, splitscreen); mDisplayMargin = context.getResources().getDimensionPixelSize( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java index 3a2f0da6bf03..60123ab97fd7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java @@ -31,12 +31,6 @@ import java.io.PrintWriter; public interface HideDisplayCutout { /** * Notifies {@link Configuration} changed. - * @param newConfig */ void onConfigurationChanged(Configuration newConfig); - - /** - * Dumps hide display cutout status. - */ - void dump(@NonNull PrintWriter pw); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java index 12b8b87f1285..23f76ca5f6ae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java @@ -44,20 +44,12 @@ public class HideDisplayCutoutController { @VisibleForTesting boolean mEnabled; - HideDisplayCutoutController(Context context, HideDisplayCutoutOrganizer organizer, - ShellExecutor mainExecutor) { - mContext = context; - mOrganizer = organizer; - mMainExecutor = mainExecutor; - updateStatus(); - } - /** * Creates {@link HideDisplayCutoutController}, returns {@code null} if the feature is not * supported. */ @Nullable - public static HideDisplayCutout create( + public static HideDisplayCutoutController create( Context context, DisplayController displayController, ShellExecutor mainExecutor) { // The SystemProperty is set for devices that support this feature and is used to control // whether to create the HideDisplayCutout instance. @@ -68,7 +60,19 @@ public class HideDisplayCutoutController { HideDisplayCutoutOrganizer organizer = new HideDisplayCutoutOrganizer(context, displayController, mainExecutor); - return new HideDisplayCutoutController(context, organizer, mainExecutor).mImpl; + return new HideDisplayCutoutController(context, organizer, mainExecutor); + } + + HideDisplayCutoutController(Context context, HideDisplayCutoutOrganizer organizer, + ShellExecutor mainExecutor) { + mContext = context; + mOrganizer = organizer; + mMainExecutor = mainExecutor; + updateStatus(); + } + + public HideDisplayCutout asHideDisplayCutout() { + return mImpl; } @VisibleForTesting @@ -94,7 +98,7 @@ public class HideDisplayCutoutController { updateStatus(); } - private void dump(@NonNull PrintWriter pw) { + public void dump(@NonNull PrintWriter pw) { final String prefix = " "; pw.print(TAG); pw.println(" states: "); @@ -111,14 +115,5 @@ public class HideDisplayCutoutController { HideDisplayCutoutController.this.onConfigurationChanged(newConfig); }); } - - @Override - public void dump(@NonNull PrintWriter pw) { - try { - mMainExecutor.executeBlocking(() -> HideDisplayCutoutController.this.dump(pw)); - } catch (InterruptedException e) { - Slog.e(TAG, "Failed to dump HideDisplayCutoutController in 2s"); - } - } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java index e13a1dbe22d6..a18d106abea4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java @@ -862,14 +862,6 @@ public class DividerView extends FrameLayout implements OnTouchListener, * assigned to it. */ private SurfaceControl getWindowSurfaceControl() { - final ViewRootImpl root = getViewRootImpl(); - if (root == null) { - return null; - } - SurfaceControl out = root.getSurfaceControl(); - if (out != null && out.isValid()) { - return out; - } return mWindowManager.mSystemWindows.getViewSurface(this); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java index bca6deb451c9..d25bef197359 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java @@ -115,21 +115,6 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays private volatile boolean mAdjustedForIme = false; private boolean mHomeStackResizable = false; - /** - * Creates {@link SplitScreen}, returns {@code null} if the feature is not supported. - */ - @Nullable - public static LegacySplitScreen create(Context context, - DisplayController displayController, SystemWindows systemWindows, - DisplayImeController imeController, TransactionPool transactionPool, - ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, - TaskStackListenerImpl taskStackListener, Transitions transitions, - ShellExecutor mainExecutor, AnimationHandler sfVsyncAnimationHandler) { - return new LegacySplitScreenController(context, displayController, systemWindows, - imeController, transactionPool, shellTaskOrganizer, syncQueue, taskStackListener, - transitions, mainExecutor, sfVsyncAnimationHandler).mImpl; - } - public LegacySplitScreenController(Context context, DisplayController displayController, SystemWindows systemWindows, DisplayImeController imeController, TransactionPool transactionPool, @@ -228,8 +213,12 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays } }); } + + public LegacySplitScreen asLegacySplitScreen() { + return mImpl; + } - void onSplitScreenSupported() { + public void onSplitScreenSupported() { // Set starting tile bounds based on middle target final WindowContainerTransaction tct = new WindowContainerTransaction(); int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position; @@ -237,7 +226,7 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays mTaskOrganizer.applyTransaction(tct); } - private void onKeyguardVisibilityChanged(boolean showing) { + public void onKeyguardVisibilityChanged(boolean showing) { if (!isSplitActive() || mView == null) { return; } @@ -293,19 +282,19 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays } } - boolean isMinimized() { + public boolean isMinimized() { return mMinimized; } - boolean isHomeStackResizable() { + public boolean isHomeStackResizable() { return mHomeStackResizable; } - DividerView getDividerView() { + public DividerView getDividerView() { return mView; } - boolean isDividerVisible() { + public boolean isDividerVisible() { return mView != null && mView.getVisibility() == View.VISIBLE; } @@ -314,13 +303,13 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays * isDividerVisible because the divider is only visible once *everything* is in split mode * while this only cares if some things are (eg. while entering/exiting as well). */ - private boolean isSplitActive() { + public boolean isSplitActive() { return mSplits.mPrimary != null && mSplits.mSecondary != null && (mSplits.mPrimary.topActivityType != ACTIVITY_TYPE_UNDEFINED || mSplits.mSecondary.topActivityType != ACTIVITY_TYPE_UNDEFINED); } - private void addDivider(Configuration configuration) { + public void addDivider(Configuration configuration) { Context dctx = mDisplayController.getDisplayContext(mContext.getDisplayId()); mView = (DividerView) LayoutInflater.from(dctx).inflate(R.layout.docked_stack_divider, null); @@ -338,14 +327,14 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays mWindowManager.add(mView, width, height, mContext.getDisplayId()); } - private void removeDivider() { + public void removeDivider() { if (mView != null) { mView.onDividerRemoved(); } mWindowManager.remove(); } - private void update(Configuration configuration) { + public void update(Configuration configuration) { final boolean isDividerHidden = mView != null && mIsKeyguardShowing; removeDivider(); @@ -358,11 +347,11 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays mView.setHidden(isDividerHidden); } - void onTaskVanished() { + public void onTaskVanished() { removeDivider(); } - private void updateVisibility(final boolean visible) { + public void updateVisibility(final boolean visible) { if (DEBUG) Slog.d(TAG, "Updating visibility " + mVisible + "->" + visible); if (mVisible != visible) { mVisible = visible; @@ -390,7 +379,7 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays } } - private void setMinimized(final boolean minimized) { + public void setMinimized(final boolean minimized) { if (DEBUG) Slog.d(TAG, "posting ext setMinimized " + minimized + " vis:" + mVisible); mMainExecutor.execute(() -> { if (DEBUG) Slog.d(TAG, "run posted ext setMinimized " + minimized + " vis:" + mVisible); @@ -401,7 +390,7 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays }); } - private void setHomeMinimized(final boolean minimized) { + public void setHomeMinimized(final boolean minimized) { if (DEBUG) { Slog.d(TAG, "setHomeMinimized min:" + mMinimized + "->" + minimized + " hrsz:" + mHomeStackResizable + " split:" + isDividerVisible()); @@ -441,7 +430,7 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays } } - void setAdjustedForIme(boolean adjustedForIme) { + public void setAdjustedForIme(boolean adjustedForIme) { if (mAdjustedForIme == adjustedForIme) { return; } @@ -449,30 +438,30 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays updateTouchable(); } - private void updateTouchable() { + public void updateTouchable() { mWindowManager.setTouchable(!mAdjustedForIme); } - private void onUndockingTask() { + public void onUndockingTask() { if (mView != null) { mView.onUndockingTask(); } } - private void onAppTransitionFinished() { + public void onAppTransitionFinished() { if (mView == null) { return; } mForcedResizableController.onAppTransitionFinished(); } - private void dump(PrintWriter pw) { + public void dump(PrintWriter pw) { pw.print(" mVisible="); pw.println(mVisible); pw.print(" mMinimized="); pw.println(mMinimized); pw.print(" mAdjustedForIme="); pw.println(mAdjustedForIme); } - long getAnimDuration() { + public long getAnimDuration() { float transitionScale = Settings.Global.getFloat(mContext.getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat( @@ -482,14 +471,14 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays return (long) (transitionDuration * transitionScale); } - void registerInSplitScreenListener(Consumer<Boolean> listener) { + public void registerInSplitScreenListener(Consumer<Boolean> listener) { listener.accept(isDividerVisible()); synchronized (mDockedStackExistsListeners) { mDockedStackExistsListeners.add(new WeakReference<>(listener)); } } - void unregisterInSplitScreenListener(Consumer<Boolean> listener) { + public void unregisterInSplitScreenListener(Consumer<Boolean> listener) { synchronized (mDockedStackExistsListeners) { for (int i = mDockedStackExistsListeners.size() - 1; i >= 0; i--) { if (mDockedStackExistsListeners.get(i) == listener) { @@ -499,13 +488,13 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays } } - private void registerBoundsChangeListener(BiConsumer<Rect, Rect> listener) { + public void registerBoundsChangeListener(BiConsumer<Rect, Rect> listener) { synchronized (mBoundsChangedListeners) { mBoundsChangedListeners.add(new WeakReference<>(listener)); } } - private boolean splitPrimaryTask() { + public boolean splitPrimaryTask() { try { if (ActivityTaskManager.getService().getLockTaskModeState() == LOCK_TASK_MODE_PINNED || isSplitActive()) { @@ -538,12 +527,12 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays topRunningTask.taskId, true /* onTop */); } - private void dismissSplitToPrimaryTask() { + public void dismissSplitToPrimaryTask() { startDismissSplit(true /* toPrimaryTask */); } /** Notifies the bounds of split screen changed. */ - void notifyBoundsChanged(Rect secondaryWindowBounds, Rect secondaryWindowInsets) { + public void notifyBoundsChanged(Rect secondaryWindowBounds, Rect secondaryWindowInsets) { synchronized (mBoundsChangedListeners) { mBoundsChangedListeners.removeIf(wf -> { BiConsumer<Rect, Rect> l = wf.get(); @@ -553,19 +542,19 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays } } - void startEnterSplit() { + public void startEnterSplit() { update(mDisplayController.getDisplayContext( mContext.getDisplayId()).getResources().getConfiguration()); // Set resizable directly here because applyEnterSplit already resizes home stack. mHomeStackResizable = mWindowManagerProxy.applyEnterSplit(mSplits, mSplitLayout); } - void prepareEnterSplitTransition(WindowContainerTransaction outWct) { + public void prepareEnterSplitTransition(WindowContainerTransaction outWct) { // Set resizable directly here because buildEnterSplit already resizes home stack. mHomeStackResizable = mWindowManagerProxy.buildEnterSplit(outWct, mSplits, mSplitLayout); } - void finishEnterSplitTransition(boolean minimized) { + public void finishEnterSplitTransition(boolean minimized) { update(mDisplayController.getDisplayContext( mContext.getDisplayId()).getResources().getConfiguration()); if (minimized) { @@ -575,11 +564,11 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays } } - void startDismissSplit(boolean toPrimaryTask) { + public void startDismissSplit(boolean toPrimaryTask) { startDismissSplit(toPrimaryTask, false /* snapped */); } - void startDismissSplit(boolean toPrimaryTask, boolean snapped) { + public void startDismissSplit(boolean toPrimaryTask, boolean snapped) { if (Transitions.ENABLE_SHELL_TRANSITIONS) { mSplits.getSplitTransitions().dismissSplit( mSplits, mSplitLayout, !toPrimaryTask, snapped); @@ -589,7 +578,7 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays } } - void onDismissSplit() { + public void onDismissSplit() { updateVisibility(false /* visible */); mMinimized = false; // Resets divider bar position to undefined, so new divider bar will apply default position @@ -599,7 +588,7 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays mImePositionProcessor.reset(); } - void ensureMinimizedSplit() { + public void ensureMinimizedSplit() { setHomeMinimized(true /* minimized */); if (mView != null && !isDividerVisible()) { // Wasn't in split-mode yet, so enter now. @@ -610,7 +599,7 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays } } - void ensureNormalSplit() { + public void ensureNormalSplit() { setHomeMinimized(false /* minimized */); if (mView != null && !isDividerVisible()) { // Wasn't in split-mode, so enter now. @@ -621,15 +610,15 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays } } - LegacySplitDisplayLayout getSplitLayout() { + public LegacySplitDisplayLayout getSplitLayout() { return mSplitLayout; } - WindowManagerProxy getWmProxy() { + public WindowManagerProxy getWmProxy() { return mWindowManagerProxy; } - WindowContainerToken getSecondaryRoot() { + public WindowContainerToken getSecondaryRoot() { if (mSplits == null || mSplits.mSecondary == null) { return null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java index 5a493c234ce3..05526018d73f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java @@ -323,6 +323,14 @@ class LegacySplitScreenTaskListener implements ShellTaskOrganizer.TaskListener { } @Override + public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { + if (!mLeashByTaskId.contains(taskId)) { + throw new IllegalArgumentException("There is no surface for taskId=" + taskId); + } + b.setParent(mLeashByTaskId.get(taskId)); + } + + @Override public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; final String childPrefix = innerPrefix + " "; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java index 94c6f018b6ac..c8f89876222e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java @@ -39,7 +39,6 @@ import android.view.WindowManagerGlobal; import android.window.TaskOrganizer; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; -import android.window.WindowOrganizer; import com.android.internal.annotations.GuardedBy; import com.android.wm.shell.common.SyncTransactionQueue; @@ -116,7 +115,7 @@ class WindowManagerProxy { void applyResizeSplits(int position, LegacySplitDisplayLayout splitLayout) { WindowContainerTransaction t = new WindowContainerTransaction(); splitLayout.resizeSplits(position, t); - new WindowOrganizer().applyTransaction(t); + applySyncTransaction(t); } boolean getHomeAndRecentsTasks(List<ActivityManager.RunningTaskInfo> out, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java index e95864873c0c..11c11f44a781 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java @@ -69,9 +69,4 @@ public interface OneHanded { * 3 button navigation mode only */ void registerGestureCallback(OneHandedGestureEventCallback callback); - - /** - * Dump one handed status. - */ - void dump(@NonNull PrintWriter pw); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java index eaa704f22410..5a3c38b09ec6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -134,10 +134,10 @@ public class OneHandedController { /** - * Creates {@link OneHanded}, returns {@code null} if the feature is not supported. + * Creates {@link OneHandedController}, returns {@code null} if the feature is not supported. */ @Nullable - public static OneHanded create( + public static OneHandedController create( Context context, DisplayController displayController, TaskStackListenerImpl taskStackListener, UiEventLogger uiEventLogger, ShellExecutor mainExecutor, Handler mainHandler) { @@ -166,7 +166,7 @@ public class OneHandedController { return new OneHandedController(context, displayController, oneHandedBackgroundPanelOrganizer, organizer, touchHandler, tutorialHandler, gestureHandler, timeoutHandler, oneHandedUiEventsLogger, overlayManager, - taskStackListener, mainExecutor, mainHandler).mImpl; + taskStackListener, mainExecutor, mainHandler); } @VisibleForTesting @@ -228,6 +228,10 @@ public class OneHandedController { mAccessibilityStateChangeListener); } + public OneHanded asOneHanded() { + return mImpl; + } + /** * Set one handed enabled or disabled when user update settings */ @@ -468,7 +472,7 @@ public class OneHandedController { } } - private void dump(@NonNull PrintWriter pw) { + public void dump(@NonNull PrintWriter pw) { final String innerPrefix = " "; pw.println(TAG + "states: "); pw.print(innerPrefix + "mOffSetFraction="); @@ -561,12 +565,5 @@ public class OneHandedController { OneHandedController.this.registerGestureCallback(callback); }); } - - @Override - public void dump(@NonNull PrintWriter pw) { - mMainExecutor.execute(() -> { - OneHandedController.this.dump(pw); - }); - } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java index 1da72f8efbb8..04d1264bdd9d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java @@ -89,6 +89,7 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { OneHandedAnimationController.OneHandedTransitionAnimator animator) { mAnimationController.removeAnimator(animator.getToken()); if (mAnimationController.isAnimatorsConsumed()) { + resetWindowsOffsetInternal(animator.getTransitionDirection()); finishOffset(animator.getDestinationOffset(), animator.getTransitionDirection()); } @@ -99,6 +100,7 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { OneHandedAnimationController.OneHandedTransitionAnimator animator) { mAnimationController.removeAnimator(animator.getToken()); if (mAnimationController.isAnimatorsConsumed()) { + resetWindowsOffsetInternal(animator.getTransitionDirection()); finishOffset(animator.getDestinationOffset(), animator.getTransitionDirection()); } @@ -205,6 +207,16 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { applyTransaction(wct); } + private void resetWindowsOffsetInternal( + @OneHandedAnimationController.TransitionDirection int td) { + if (td == TRANSITION_DIRECTION_TRIGGER) { + return; + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); + resetWindowsOffset(wct); + applyTransaction(wct); + } + private void resetWindowsOffset(WindowContainerTransaction wct) { final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index 90992fb92324..5ffa9885a143 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -16,18 +16,24 @@ package com.android.wm.shell.pip; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + import android.animation.AnimationHandler; import android.animation.Animator; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.annotation.IntDef; +import android.app.TaskInfo; import android.graphics.Rect; import android.view.Choreographer; +import android.view.Surface; import android.view.SurfaceControl; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.common.DisplayLayout; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -99,18 +105,20 @@ public class PipAnimationController { @SuppressWarnings("unchecked") @VisibleForTesting - public PipTransitionAnimator getAnimator(SurfaceControl leash, + public PipTransitionAnimator getAnimator(TaskInfo taskInfo, SurfaceControl leash, Rect destinationBounds, float alphaStart, float alphaEnd) { if (mCurrentAnimator == null) { mCurrentAnimator = setupPipTransitionAnimator( - PipTransitionAnimator.ofAlpha(leash, destinationBounds, alphaStart, alphaEnd)); + PipTransitionAnimator.ofAlpha(taskInfo, leash, destinationBounds, alphaStart, + alphaEnd)); } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA && mCurrentAnimator.isRunning()) { mCurrentAnimator.updateEndValue(alphaEnd); } else { mCurrentAnimator.cancel(); mCurrentAnimator = setupPipTransitionAnimator( - PipTransitionAnimator.ofAlpha(leash, destinationBounds, alphaStart, alphaEnd)); + PipTransitionAnimator.ofAlpha(taskInfo, leash, destinationBounds, alphaStart, + alphaEnd)); } return mCurrentAnimator; } @@ -129,15 +137,21 @@ public class PipAnimationController { * leash bounds before transformation/any animation. This is so when we try to construct * the different transformation matrices for the animation, we are constructing this based off * the PiP original bounds, rather than the {@param startBounds}, which is post-transformed. + * + * If non-zero {@param rotationDelta} is given, it means that the display will be rotated by + * leaving PiP to fullscreen, and the {@param endBounds} is the fullscreen bounds before the + * rotation change. */ @VisibleForTesting - public PipTransitionAnimator getAnimator(SurfaceControl leash, Rect baseBounds, - Rect startBounds, Rect endBounds, Rect sourceHintRect, - @PipAnimationController.TransitionDirection int direction, float startingAngle) { + public PipTransitionAnimator getAnimator(TaskInfo taskInfo, SurfaceControl leash, + Rect baseBounds, Rect startBounds, Rect endBounds, Rect sourceHintRect, + @PipAnimationController.TransitionDirection int direction, float startingAngle, + @Surface.Rotation int rotationDelta) { if (mCurrentAnimator == null) { mCurrentAnimator = setupPipTransitionAnimator( - PipTransitionAnimator.ofBounds(leash, startBounds, startBounds, endBounds, - sourceHintRect, direction, 0 /* startingAngle */)); + PipTransitionAnimator.ofBounds(taskInfo, leash, startBounds, startBounds, + endBounds, sourceHintRect, direction, 0 /* startingAngle */, + rotationDelta)); } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA && mCurrentAnimator.isRunning()) { // If we are still animating the fade into pip, then just move the surface and ensure @@ -152,8 +166,8 @@ public class PipAnimationController { } else { mCurrentAnimator.cancel(); mCurrentAnimator = setupPipTransitionAnimator( - PipTransitionAnimator.ofBounds(leash, baseBounds, startBounds, endBounds, - sourceHintRect, direction, startingAngle)); + PipTransitionAnimator.ofBounds(taskInfo, leash, baseBounds, startBounds, + endBounds, sourceHintRect, direction, startingAngle, rotationDelta)); } return mCurrentAnimator; } @@ -177,18 +191,18 @@ public class PipAnimationController { /** * Called when PiP animation is started. */ - public void onPipAnimationStart(PipTransitionAnimator animator) {} + public void onPipAnimationStart(TaskInfo taskInfo, PipTransitionAnimator animator) {} /** * Called when PiP animation is ended. */ - public void onPipAnimationEnd(SurfaceControl.Transaction tx, + public void onPipAnimationEnd(TaskInfo taskInfo, SurfaceControl.Transaction tx, PipTransitionAnimator animator) {} /** * Called when PiP animation is cancelled. */ - public void onPipAnimationCancel(PipTransitionAnimator animator) {} + public void onPipAnimationCancel(TaskInfo taskInfo, PipTransitionAnimator animator) {} } /** @@ -198,6 +212,7 @@ public class PipAnimationController { public abstract static class PipTransitionAnimator<T> extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener { + private final TaskInfo mTaskInfo; private final SurfaceControl mLeash; private final @AnimationType int mAnimationType; private final Rect mDestinationBounds = new Rect(); @@ -213,9 +228,10 @@ public class PipAnimationController { private PipSurfaceTransactionHelper mSurfaceTransactionHelper; private @TransitionDirection int mTransitionDirection; - private PipTransitionAnimator(SurfaceControl leash, @AnimationType int animationType, - Rect destinationBounds, T baseValue, T startValue, T endValue, - float startingAngle) { + private PipTransitionAnimator(TaskInfo taskInfo, SurfaceControl leash, + @AnimationType int animationType, Rect destinationBounds, T baseValue, T startValue, + T endValue, float startingAngle) { + mTaskInfo = taskInfo; mLeash = leash; mAnimationType = animationType; mDestinationBounds.set(destinationBounds); @@ -234,7 +250,7 @@ public class PipAnimationController { mCurrentValue = mStartValue; onStartTransaction(mLeash, newSurfaceControlTransaction()); if (mPipAnimationCallback != null) { - mPipAnimationCallback.onPipAnimationStart(this); + mPipAnimationCallback.onPipAnimationStart(mTaskInfo, this); } } @@ -250,14 +266,14 @@ public class PipAnimationController { final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); onEndTransaction(mLeash, tx, mTransitionDirection); if (mPipAnimationCallback != null) { - mPipAnimationCallback.onPipAnimationEnd(tx, this); + mPipAnimationCallback.onPipAnimationEnd(mTaskInfo, tx, this); } } @Override public void onAnimationCancel(Animator animation) { if (mPipAnimationCallback != null) { - mPipAnimationCallback.onPipAnimationCancel(this); + mPipAnimationCallback.onPipAnimationCancel(mTaskInfo, this); } } @@ -368,9 +384,9 @@ public class PipAnimationController { abstract void applySurfaceControlTransaction(SurfaceControl leash, SurfaceControl.Transaction tx, float fraction); - static PipTransitionAnimator<Float> ofAlpha(SurfaceControl leash, + static PipTransitionAnimator<Float> ofAlpha(TaskInfo taskInfo, SurfaceControl leash, Rect destinationBounds, float startValue, float endValue) { - return new PipTransitionAnimator<Float>(leash, ANIM_TYPE_ALPHA, + return new PipTransitionAnimator<Float>(taskInfo, leash, ANIM_TYPE_ALPHA, destinationBounds, startValue, startValue, endValue, 0) { @Override void applySurfaceControlTransaction(SurfaceControl leash, @@ -403,9 +419,10 @@ public class PipAnimationController { }; } - static PipTransitionAnimator<Rect> ofBounds(SurfaceControl leash, + static PipTransitionAnimator<Rect> ofBounds(TaskInfo taskInfo, SurfaceControl leash, Rect baseValue, Rect startValue, Rect endValue, Rect sourceHintRect, - @PipAnimationController.TransitionDirection int direction, float startingAngle) { + @PipAnimationController.TransitionDirection int direction, float startingAngle, + @Surface.Rotation int rotationDelta) { // Just for simplicity we'll interpolate between the source rect hint insets and empty // insets to calculate the window crop final Rect initialSourceValue; @@ -426,8 +443,18 @@ public class PipAnimationController { } final Rect sourceInsets = new Rect(0, 0, 0, 0); + final Rect rotatedEndRect; + if (rotationDelta == ROTATION_90 || rotationDelta == ROTATION_270) { + // Rotate the end bounds according to the rotation delta because the display will + // be rotated to the same orientation. + rotatedEndRect = new Rect(endValue); + DisplayLayout.rotateBounds(rotatedEndRect, endValue, rotationDelta); + } else { + rotatedEndRect = null; + } + // construct new Rect instances in case they are recycled - return new PipTransitionAnimator<Rect>(leash, ANIM_TYPE_BOUNDS, + return new PipTransitionAnimator<Rect>(taskInfo, leash, ANIM_TYPE_BOUNDS, endValue, new Rect(baseValue), new Rect(startValue), new Rect(endValue), startingAngle) { private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect()); @@ -439,6 +466,12 @@ public class PipAnimationController { final Rect base = getBaseValue(); final Rect start = getStartValue(); final Rect end = getEndValue(); + if (rotatedEndRect != null) { + // Animate the bounds in a different orientation. It only happens when + // leaving PiP to fullscreen. + applyRotation(tx, leash, fraction, start, end, rotatedEndRect); + return; + } Rect bounds = mRectEvaluator.evaluate(fraction, start, end); float angle = (1.0f - fraction) * startingAngle; setCurrentValue(bounds); @@ -464,6 +497,25 @@ public class PipAnimationController { tx.apply(); } + private void applyRotation(SurfaceControl.Transaction tx, SurfaceControl leash, + float fraction, Rect start, Rect end, Rect rotatedEndRect) { + final Rect bounds = mRectEvaluator.evaluate(fraction, start, rotatedEndRect); + setCurrentValue(bounds); + final float degree, x, y; + if (rotationDelta == ROTATION_90) { + degree = 90 * fraction; + x = fraction * (end.right - start.left) + start.left; + y = fraction * (end.top - start.top) + start.top; + } else { + degree = -90 * fraction; + x = fraction * (end.left - start.left) + start.left; + y = fraction * (end.bottom - start.top) + start.top; + } + getSurfaceTransactionHelper().rotateAndScaleWithCrop(tx, leash, bounds, + rotatedEndRect, degree, x, y); + tx.apply(); + } + @Override void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { getSurfaceTransactionHelper() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java index a8961ea3d8a8..ac5d14c802d8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java @@ -19,7 +19,9 @@ package com.android.wm.shell.pip; import static android.util.TypedValue.COMPLEX_UNIT_DIP; import android.annotation.NonNull; +import android.app.PictureInPictureParams; import android.content.Context; +import android.content.pm.ActivityInfo; import android.content.res.Resources; import android.graphics.Point; import android.graphics.PointF; @@ -27,7 +29,6 @@ import android.graphics.Rect; import android.util.DisplayMetrics; import android.util.Size; import android.util.TypedValue; -import android.view.DisplayInfo; import android.view.Gravity; import com.android.wm.shell.common.DisplayLayout; @@ -142,11 +143,53 @@ public class PipBoundsAlgorithm { true /* useCurrentMinEdgeSize */, false /* useCurrentSize */); } + /** + * + * Get the smallest/most minimal size allowed. + */ + public Size getMinimalSize(ActivityInfo activityInfo) { + if (activityInfo == null || activityInfo.windowLayout == null) { + return null; + } + final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout; + // -1 will be populated if an activity specifies defaultWidth/defaultHeight in <layout> + // without minWidth/minHeight + if (windowLayout.minWidth > 0 && windowLayout.minHeight > 0) { + return new Size(windowLayout.minWidth, windowLayout.minHeight); + } + return null; + } + + /** + * Returns the source hint rect if it is valid (if provided and is contained by the current + * task bounds). + */ + public static Rect getValidSourceHintRect(PictureInPictureParams params, Rect sourceBounds) { + final Rect sourceHintRect = params != null && params.hasSourceBoundsHint() + ? params.getSourceRectHint() + : null; + if (sourceHintRect != null && sourceBounds.contains(sourceHintRect)) { + return sourceHintRect; + } + return null; + } + public float getDefaultAspectRatio() { return mDefaultAspectRatio; } /** + * + * Give the aspect ratio if the supplied PiP params have one, or else return default. + */ + public float getAspectRatioOrDefault( + @android.annotation.Nullable PictureInPictureParams params) { + return params != null && params.hasSetAspectRatio() + ? params.getAspectRatio() + : getDefaultAspectRatio(); + } + + /** * @return whether the given {@param aspectRatio} is valid. */ private boolean isValidPictureInPictureAspectRatio(float aspectRatio) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java index b112c51455d2..cb39b4e63655 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java @@ -25,7 +25,6 @@ import android.graphics.Point; import android.graphics.Rect; import android.util.Size; import android.view.Display; -import android.view.DisplayInfo; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.function.TriConsumer; @@ -344,6 +343,16 @@ public final class PipBoundsState { } } + /** + * Initialize states when first entering PiP. + */ + public void setBoundsStateForEntry(ComponentName componentName, float aspectRatio, + Size overrideMinSize) { + setLastPipComponentName(componentName); + setAspectRatio(aspectRatio); + setOverrideMinSize(overrideMinSize); + } + /** Returns whether the shelf is currently showing. */ public boolean isShelfShowing() { return mIsShelfShowing; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java index a777a2766ee7..97aeda4b053f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -127,6 +127,32 @@ public class PipSurfaceTransactionHelper { } /** + * Operates the rotation according to the given degrees and scale (setMatrix) according to the + * source bounds and rotated destination bounds. The crop will be the unscaled source bounds. + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper rotateAndScaleWithCrop(SurfaceControl.Transaction tx, + SurfaceControl leash, Rect sourceBounds, Rect destinationBounds, float degrees, + float positionX, float positionY) { + mTmpDestinationRect.set(sourceBounds); + final int dw = destinationBounds.width(); + final int dh = destinationBounds.height(); + // Scale by the short side so there won't be empty area if the aspect ratio of source and + // destination are different. + final float scale = dw <= dh + ? (float) sourceBounds.width() / dw + : (float) sourceBounds.height() / dh; + // Inverse scale for crop to fit in screen coordinates. + mTmpDestinationRect.scale(1 / scale); + mTmpTransform.setRotate(degrees); + mTmpTransform.postScale(scale, scale); + mTmpTransform.postTranslate(positionX, positionY); + tx.setMatrix(leash, mTmpTransform, mTmpFloat9) + .setWindowCrop(leash, mTmpDestinationRect.width(), mTmpDestinationRect.height()); + return this; + } + + /** * Resets the scale (setMatrix) on a given transaction and leash if there's any * * @return same {@link PipSurfaceTransactionHelper} instance for method chaining diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index b7958b7a7077..71331dfcef44 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -41,21 +41,19 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.NonNull; -import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.PictureInPictureParams; +import android.app.TaskInfo; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; -import android.content.res.Configuration; import android.graphics.Rect; -import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import android.util.Rational; -import android.util.Size; import android.view.Display; +import android.view.Surface; import android.view.SurfaceControl; import android.window.TaskOrganizer; import android.window.WindowContainerToken; @@ -63,19 +61,16 @@ import android.window.WindowContainerTransaction; import android.window.WindowContainerTransactionCallback; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ShellMainThread; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.pip.phone.PipMotionHelper; +import com.android.wm.shell.transition.Transitions; + import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; @@ -132,12 +127,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private final PipBoundsAlgorithm mPipBoundsAlgorithm; private final @NonNull PipMenuController mPipMenuController; private final PipAnimationController mPipAnimationController; + private final PipTransitionController mPipTransitionController; private final PipUiEventLogger mPipUiEventLoggerLogger; - private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); private final int mEnterExitAnimationDuration; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; - private final Map<IBinder, Configuration> mInitialState = new HashMap<>(); - private final Optional<LegacySplitScreen> mSplitScreenOptional; + private final Optional<LegacySplitScreenController> mSplitScreenOptional; protected final ShellTaskOrganizer mTaskOrganizer; protected final ShellExecutor mMainExecutor; @@ -145,7 +139,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private final PipAnimationController.PipAnimationCallback mPipAnimationCallback = new PipAnimationController.PipAnimationCallback() { @Override - public void onPipAnimationStart(PipAnimationController.PipTransitionAnimator animator) { + public void onPipAnimationStart(TaskInfo taskInfo, + PipAnimationController.PipTransitionAnimator animator) { final int direction = animator.getTransitionDirection(); if (direction == TRANSITION_DIRECTION_TO_PIP) { // TODO (b//169221267): Add jank listener for transactions without buffer updates. @@ -156,7 +151,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } @Override - public void onPipAnimationEnd(SurfaceControl.Transaction tx, + public void onPipAnimationEnd(TaskInfo taskInfo, SurfaceControl.Transaction tx, PipAnimationController.PipTransitionAnimator animator) { final int direction = animator.getTransitionDirection(); finishResize(tx, animator.getDestinationBounds(), direction, @@ -170,7 +165,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } @Override - public void onPipAnimationCancel(PipAnimationController.PipTransitionAnimator animator) { + public void onPipAnimationCancel(TaskInfo taskInfo, + PipAnimationController.PipTransitionAnimator animator) { sendOnPipTransitionCancelled(animator.getTransitionDirection()); } }; @@ -192,6 +188,12 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private boolean mWaitForFixedRotation; /** + * The rotation that the display will apply after expanding PiP to fullscreen. This is only + * meaningful if {@link #mWaitForFixedRotation} is true. + */ + private @Surface.Rotation int mNextRotation; + + /** * If set to {@code true}, no entering PiP transition would be kicked off and most likely * it's due to the fact that Launcher is handling the transition directly when swiping * auto PiP-able Activity to home. @@ -202,8 +204,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public PipTaskOrganizer(Context context, @NonNull PipBoundsState pipBoundsState, @NonNull PipBoundsAlgorithm boundsHandler, @NonNull PipMenuController pipMenuController, + @NonNull PipAnimationController pipAnimationController, @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, - Optional<LegacySplitScreen> splitScreenOptional, + @NonNull PipTransitionController pipTransitionController, + Optional<LegacySplitScreenController> splitScreenOptional, @NonNull DisplayController displayController, @NonNull PipUiEventLogger pipUiEventLogger, @NonNull ShellTaskOrganizer shellTaskOrganizer, @@ -211,10 +215,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipBoundsState = pipBoundsState; mPipBoundsAlgorithm = boundsHandler; mPipMenuController = pipMenuController; + mPipTransitionController = pipTransitionController; mEnterExitAnimationDuration = context.getResources() .getInteger(R.integer.config_pipResizeAnimationDuration); mSurfaceTransactionHelper = surfaceTransactionHelper; - mPipAnimationController = new PipAnimationController(mSurfaceTransactionHelper); + mPipAnimationController = pipAnimationController; mPipUiEventLoggerLogger = pipUiEventLogger; mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; mSplitScreenOptional = splitScreenOptional; @@ -246,13 +251,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** - * Registers {@link PipTransitionCallback} to receive transition callbacks. - */ - public void registerPipTransitionCallback(PipTransitionCallback callback) { - mPipTransitionCallbacks.add(callback); - } - - /** * Registers a callback when a display change has been detected when we enter PiP. */ public void registerOnDisplayIdChangeCallback(IntConsumer onDisplayIdChangeCallback) { @@ -275,7 +273,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, PictureInPictureParams pictureInPictureParams) { mInSwipePipToHomeTransition = true; - sendOnPipTransitionStarted(componentName, TRANSITION_DIRECTION_TO_PIP); + sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); setBoundsStateForEntry(componentName, pictureInPictureParams, activityInfo); // disable the conflicting transaction from fixed rotation, see also // onFixedRotationStarted and onFixedRotationFinished @@ -296,9 +294,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private void setBoundsStateForEntry(ComponentName componentName, PictureInPictureParams params, ActivityInfo activityInfo) { - mPipBoundsState.setLastPipComponentName(componentName); - mPipBoundsState.setAspectRatio(getAspectRatioOrDefault(params)); - mPipBoundsState.setOverrideMinSize(getMinimalSize(activityInfo)); + mPipBoundsState.setBoundsStateForEntry(componentName, + mPipBoundsAlgorithm.getAspectRatioOrDefault(params), + mPipBoundsAlgorithm.getMinimalSize(activityInfo)); } /** @@ -317,61 +315,40 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return; } - final Configuration initialConfig = mInitialState.remove(mToken.asBinder()); - if (initialConfig == null) { - Log.wtf(TAG, "Token not in record, this should not happen mToken=" + mToken); - return; - } mPipUiEventLoggerLogger.log( PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN); - final boolean orientationDiffers = initialConfig.windowConfiguration.getRotation() - != mPipBoundsState.getDisplayLayout().rotation(); final WindowContainerTransaction wct = new WindowContainerTransaction(); - final Rect destinationBounds = initialConfig.windowConfiguration.getBounds(); + final Rect destinationBounds = mPipBoundsState.getDisplayBounds(); final int direction = syncWithSplitScreenBounds(destinationBounds) ? TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN : TRANSITION_DIRECTION_LEAVE_PIP; - if (orientationDiffers) { - mState = State.EXITING_PIP; - // Send started callback though animation is ignored. - sendOnPipTransitionStarted(direction); - // Don't bother doing an animation if the display rotation differs or if it's in - // a non-supported windowing mode - applyWindowingModeChangeOnExit(wct, direction); - mTaskOrganizer.applyTransaction(wct); - // Send finished callback though animation is ignored. - sendOnPipTransitionFinished(direction); - } else { - final SurfaceControl.Transaction tx = - mSurfaceControlTransactionFactory.getTransaction(); - mSurfaceTransactionHelper.scale(tx, mLeash, destinationBounds, - mPipBoundsState.getBounds()); - tx.setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height()); - // We set to fullscreen here for now, but later it will be set to UNDEFINED for - // the proper windowing mode to take place. See #applyWindowingModeChangeOnExit. - wct.setActivityWindowingMode(mToken, - direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN - ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY - : WINDOWING_MODE_FULLSCREEN); - wct.setBounds(mToken, destinationBounds); - wct.setBoundsChangeTransaction(mToken, tx); - mTaskOrganizer.applySyncTransaction(wct, new WindowContainerTransactionCallback() { - @Override - public void onTransactionReady(int id, SurfaceControl.Transaction t) { - mMainExecutor.execute(() -> { - t.apply(); - // Make sure to grab the latest source hint rect as it could have been - // updated right after applying the windowing mode change. - final Rect sourceHintRect = getValidSourceHintRect(mPictureInPictureParams, - destinationBounds); - scheduleAnimateResizePip(mPipBoundsState.getBounds(), destinationBounds, - 0 /* startingAngle */, sourceHintRect, direction, - animationDurationMs, null /* updateBoundsCallback */); - mState = State.EXITING_PIP; - }); - } - }); - } + final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); + mSurfaceTransactionHelper.scale(tx, mLeash, destinationBounds, mPipBoundsState.getBounds()); + tx.setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height()); + // We set to fullscreen here for now, but later it will be set to UNDEFINED for + // the proper windowing mode to take place. See #applyWindowingModeChangeOnExit. + wct.setActivityWindowingMode(mToken, + direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN + ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY + : WINDOWING_MODE_FULLSCREEN); + wct.setBounds(mToken, destinationBounds); + wct.setBoundsChangeTransaction(mToken, tx); + mTaskOrganizer.applySyncTransaction(wct, new WindowContainerTransactionCallback() { + @Override + public void onTransactionReady(int id, SurfaceControl.Transaction t) { + mMainExecutor.execute(() -> { + t.apply(); + // Make sure to grab the latest source hint rect as it could have been + // updated right after applying the windowing mode change. + final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( + mPictureInPictureParams, destinationBounds); + scheduleAnimateResizePip(mPipBoundsState.getBounds(), destinationBounds, + 0 /* startingAngle */, sourceHintRect, direction, + animationDurationMs, null /* updateBoundsCallback */); + mState = State.EXITING_PIP; + }); + } + }); } private void applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction) { @@ -398,12 +375,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // removePipImmediately is expected when the following animation finishes. mPipAnimationController - .getAnimator(mLeash, mPipBoundsState.getBounds(), 1f, 0f) + .getAnimator(mTaskInfo, mLeash, mPipBoundsState.getBounds(), 1f, 0f) .setTransitionDirection(TRANSITION_DIRECTION_REMOVE_STACK) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(mEnterExitAnimationDuration) .start(); - mInitialState.remove(mToken.asBinder()); mState = State.EXITING_PIP; } @@ -428,7 +404,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mToken = mTaskInfo.token; mState = State.TASK_APPEARED; mLeash = leash; - mInitialState.put(mToken.asBinder(), new Configuration(mTaskInfo.configuration)); mPictureInPictureParams = mTaskInfo.pictureInPictureParams; setBoundsStateForEntry(mTaskInfo.topActivity, mPictureInPictureParams, mTaskInfo.topActivityInfo); @@ -470,10 +445,17 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, Objects.requireNonNull(destinationBounds, "Missing destination bounds"); final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { + mPipMenuController.attach(mLeash); + } + return; + } + if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { mPipMenuController.attach(mLeash); - final Rect sourceHintRect = getValidSourceHintRect(info.pictureInPictureParams, - currentBounds); + final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( + info.pictureInPictureParams, currentBounds); scheduleAnimateResizePip(currentBounds, destinationBounds, 0 /* startingAngle */, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterExitAnimationDuration, null /* updateBoundsCallback */); @@ -486,21 +468,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } } - /** - * Returns the source hint rect if it is valid (if provided and is contained by the current - * task bounds). - */ - private Rect getValidSourceHintRect(PictureInPictureParams params, Rect sourceBounds) { - final Rect sourceHintRect = params != null - && params.hasSourceBoundsHint() - ? params.getSourceRectHint() - : null; - if (sourceHintRect != null && sourceBounds.contains(sourceHintRect)) { - return sourceHintRect; - } - return null; - } - @VisibleForTesting void enterPipWithAlphaAnimation(Rect destinationBounds, long durationMs) { // If we are fading the PIP in, then we should move the pip to the final location as @@ -512,7 +479,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, tx.apply(); applyEnterPipSyncTransaction(destinationBounds, () -> { mPipAnimationController - .getAnimator(mLeash, destinationBounds, 0f, 1f) + .getAnimator(mTaskInfo, mLeash, destinationBounds, 0f, 1f) .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(durationMs) @@ -547,19 +514,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private void sendOnPipTransitionStarted( @PipAnimationController.TransitionDirection int direction) { - sendOnPipTransitionStarted(mTaskInfo.baseActivity, direction); - } - - private void sendOnPipTransitionStarted(ComponentName componentName, - @PipAnimationController.TransitionDirection int direction) { if (direction == TRANSITION_DIRECTION_TO_PIP) { mState = State.ENTERING_PIP; } - final Rect pipBounds = mPipBoundsState.getBounds(); - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionStarted(componentName, direction, pipBounds); - } + mPipTransitionController.sendOnPipTransitionStarted(direction); } private void sendOnPipTransitionFinished( @@ -567,18 +525,12 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (direction == TRANSITION_DIRECTION_TO_PIP) { mState = State.ENTERED_PIP; } - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionFinished(mTaskInfo.baseActivity, direction); - } + mPipTransitionController.sendOnPipTransitionFinished(direction); } private void sendOnPipTransitionCancelled( @PipAnimationController.TransitionDirection int direction) { - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionCanceled(mTaskInfo.baseActivity, direction); - } + mPipTransitionController.sendOnPipTransitionCancelled(direction); } /** @@ -616,7 +568,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); mPipBoundsState.setLastPipComponentName(info.topActivity); - mPipBoundsState.setOverrideMinSize(getMinimalSize(info.topActivityInfo)); + mPipBoundsState.setOverrideMinSize( + mPipBoundsAlgorithm.getMinimalSize(info.topActivityInfo)); final PictureInPictureParams newParams = info.pictureInPictureParams; if (newParams == null || !applyPictureInPictureParams(newParams)) { Log.d(TAG, "Ignored onTaskInfoChanged with PiP param: " + newParams); @@ -631,7 +584,14 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } @Override + public boolean supportSizeCompatUI() { + // PIP doesn't support size compat. + return false; + } + + @Override public void onFixedRotationStarted(int displayId, int newRotation) { + mNextRotation = newRotation; mWaitForFixedRotation = true; } @@ -671,7 +631,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipAnimationController.getCurrentAnimator(); if (animator == null || !animator.isRunning() || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) { - if (mState.isInPip() && fromRotation) { + if (mState.isInPip() && fromRotation && !mWaitForFixedRotation) { // Update bounds state to final destination first. It's important to do this // before finishing & cancelling the transition animation so that the MotionHelper // bounds are synchronized to the destination bounds when the animation ends. @@ -1078,38 +1038,23 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, Log.w(TAG, "Abort animation, invalid leash"); return; } + final int rotationDelta = mWaitForFixedRotation + ? ((mNextRotation - mPipBoundsState.getDisplayLayout().rotation()) + 4) % 4 + : Surface.ROTATION_0; Rect baseBounds = direction == TRANSITION_DIRECTION_SNAP_AFTER_RESIZE ? mPipBoundsState.getBounds() : currentBounds; mPipAnimationController - .getAnimator(mLeash, baseBounds, currentBounds, destinationBounds, sourceHintRect, - direction, startingAngle) + .getAnimator(mTaskInfo, mLeash, baseBounds, currentBounds, destinationBounds, + sourceHintRect, direction, startingAngle, rotationDelta) .setTransitionDirection(direction) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(durationMs) .start(); } - private Size getMinimalSize(ActivityInfo activityInfo) { - if (activityInfo == null || activityInfo.windowLayout == null) { - return null; - } - final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout; - // -1 will be populated if an activity specifies defaultWidth/defaultHeight in <layout> - // without minWidth/minHeight - if (windowLayout.minWidth > 0 && windowLayout.minHeight > 0) { - return new Size(windowLayout.minWidth, windowLayout.minHeight); - } - return null; - } - - private float getAspectRatioOrDefault(@Nullable PictureInPictureParams params) { - return params == null || !params.hasSetAspectRatio() - ? mPipBoundsAlgorithm.getDefaultAspectRatio() - : params.getAspectRatio(); - } - /** - * Sync with {@link LegacySplitScreen} on destination bounds if PiP is going to split screen. + * Sync with {@link LegacySplitScreenController} on destination bounds if PiP is going to split + * screen. * * @param destinationBoundsOut contain the updated destination bounds if applicable * @return {@code true} if destinationBounds is altered for split screen @@ -1119,7 +1064,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return false; } - LegacySplitScreen legacySplitScreen = mSplitScreenOptional.get(); + LegacySplitScreenController legacySplitScreen = mSplitScreenOptional.get(); if (!legacySplitScreen.isDividerVisible()) { // fail early if system is not in split screen mode return false; @@ -1146,35 +1091,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, pw.println(innerPrefix + "mState=" + mState); pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType); pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams); - pw.println(innerPrefix + "mInitialState:"); - for (Map.Entry<IBinder, Configuration> e : mInitialState.entrySet()) { - pw.println(innerPrefix + " binder=" + e.getKey() - + " winConfig=" + e.getValue().windowConfiguration); - } } @Override public String toString() { return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_PIP); } - - /** - * Callback interface for PiP transitions (both from and to PiP mode) - */ - public interface PipTransitionCallback { - /** - * Callback when the pip transition is started. - */ - void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds); - - /** - * Callback when the pip transition is finished. - */ - void onPipTransitionFinished(ComponentName activity, int direction); - - /** - * Callback when the pip transition is cancelled. - */ - void onPipTransitionCanceled(ComponentName activity, int direction); - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java new file mode 100644 index 000000000000..9b6909b2bd51 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2021 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.wm.shell.pip; + +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; +import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_BOUNDS; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; +import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; +import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; + +import android.app.TaskInfo; +import android.content.Context; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.Surface; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.transition.Transitions; + +/** + * Implementation of transitions for PiP on phone. Responsible for enter (alpha, bounds) and + * exit animation. + */ +public class PipTransition extends PipTransitionController { + + private final int mEnterExitAnimationDuration; + private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; + private Transitions.TransitionFinishCallback mFinishCallback; + + public PipTransition(Context context, + PipBoundsState pipBoundsState, PipMenuController pipMenuController, + PipBoundsAlgorithm pipBoundsAlgorithm, + PipAnimationController pipAnimationController, + Transitions transitions, + @NonNull ShellTaskOrganizer shellTaskOrganizer) { + super(pipBoundsState, pipMenuController, pipBoundsAlgorithm, + pipAnimationController, transitions, shellTaskOrganizer); + mEnterExitAnimationDuration = context.getResources() + .getInteger(R.integer.config_pipResizeAnimationDuration); + } + + @Override + public boolean startAnimation(@android.annotation.NonNull IBinder transition, + @android.annotation.NonNull TransitionInfo info, + @android.annotation.NonNull SurfaceControl.Transaction t, + @android.annotation.NonNull Transitions.TransitionFinishCallback finishCallback) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getTaskInfo() != null + && change.getTaskInfo().configuration.windowConfiguration.getWindowingMode() + == WINDOWING_MODE_PINNED) { + mFinishCallback = finishCallback; + return startEnterAnimation(change.getTaskInfo(), change.getLeash(), t); + } + } + return false; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } + + @Override + public void onFinishResize(TaskInfo taskInfo, Rect destinationBounds, + @PipAnimationController.TransitionDirection int direction, + SurfaceControl.Transaction tx) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareFinishResizeTransaction(taskInfo, destinationBounds, + direction, tx, wct); + mFinishCallback.onTransitionFinished(wct, null); + finishResizeForMenu(destinationBounds); + } + + private boolean startEnterAnimation(final TaskInfo taskInfo, final SurfaceControl leash, + final SurfaceControl.Transaction t) { + setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams, + taskInfo.topActivityInfo); + final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + final Rect currentBounds = taskInfo.configuration.windowConfiguration.getBounds(); + PipAnimationController.PipTransitionAnimator animator; + if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { + final Rect sourceHintRect = + PipBoundsAlgorithm.getValidSourceHintRect( + taskInfo.pictureInPictureParams, currentBounds); + animator = mPipAnimationController.getAnimator(taskInfo, leash, + currentBounds, currentBounds, destinationBounds, sourceHintRect, + TRANSITION_DIRECTION_TO_PIP, 0 /* startingAngle */, Surface.ROTATION_0); + } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { + t.setAlpha(leash, 0f); + t.apply(); + animator = mPipAnimationController.getAnimator(taskInfo, leash, + destinationBounds, 0f, 1f); + mOneShotAnimationType = ANIM_TYPE_BOUNDS; + } else { + throw new RuntimeException("Unrecognized animation type: " + + mOneShotAnimationType); + } + animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) + .setPipAnimationCallback(mPipAnimationCallback) + .setDuration(mEnterExitAnimationDuration) + .start(); + return true; + } + + private void finishResizeForMenu(Rect destinationBounds) { + mPipMenuController.movePipMenu(null, null, destinationBounds); + mPipMenuController.updateMenuBounds(destinationBounds); + } + + private void prepareFinishResizeTransaction(TaskInfo taskInfo, Rect destinationBounds, + @PipAnimationController.TransitionDirection int direction, + SurfaceControl.Transaction tx, + WindowContainerTransaction wct) { + Rect taskBounds = null; + if (isInPipDirection(direction)) { + // If we are animating from fullscreen using a bounds animation, then reset the + // activity windowing mode set by WM, and set the task bounds to the final bounds + taskBounds = destinationBounds; + wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); + wct.scheduleFinishEnterPip(taskInfo.token, destinationBounds); + } else if (isOutPipDirection(direction)) { + // If we are animating to fullscreen, then we need to reset the override bounds + // on the task to ensure that the task "matches" the parent's bounds. + taskBounds = (direction == TRANSITION_DIRECTION_LEAVE_PIP) + ? null : destinationBounds; + wct.setWindowingMode(taskInfo.token, getOutPipWindowingMode()); + // Simply reset the activity mode set prior to the animation running. + wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); + } + + wct.setBounds(taskInfo.token, taskBounds); + wct.setBoundsChangeTransaction(taskInfo.token, tx); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java new file mode 100644 index 000000000000..d801c918973a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2021 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.wm.shell.pip; + +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; + +import android.app.PictureInPictureParams; +import android.app.TaskInfo; +import android.content.ComponentName; +import android.content.pm.ActivityInfo; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.view.SurfaceControl; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.transition.Transitions; + +import java.util.ArrayList; +import java.util.List; + +/** + * Responsible supplying PiP Transitions. + */ +public abstract class PipTransitionController implements Transitions.TransitionHandler { + + protected final PipAnimationController mPipAnimationController; + protected final PipBoundsAlgorithm mPipBoundsAlgorithm; + protected final PipBoundsState mPipBoundsState; + protected final ShellTaskOrganizer mShellTaskOrganizer; + protected final PipMenuController mPipMenuController; + private final Handler mMainHandler; + private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); + + protected final PipAnimationController.PipAnimationCallback mPipAnimationCallback = + new PipAnimationController.PipAnimationCallback() { + @Override + public void onPipAnimationStart(TaskInfo taskInfo, + PipAnimationController.PipTransitionAnimator animator) { + final int direction = animator.getTransitionDirection(); + if (direction == TRANSITION_DIRECTION_TO_PIP) { + // TODO (b//169221267): Add jank listener for transactions without buffer + // updates. + //InteractionJankMonitor.getInstance().begin( + // InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP, 2000); + } + sendOnPipTransitionStarted(direction); + } + + @Override + public void onPipAnimationEnd(TaskInfo taskInfo, SurfaceControl.Transaction tx, + PipAnimationController.PipTransitionAnimator animator) { + final int direction = animator.getTransitionDirection(); + mPipBoundsState.setBounds(animator.getDestinationBounds()); + if (direction == TRANSITION_DIRECTION_REMOVE_STACK) { + return; + } + onFinishResize(taskInfo, animator.getDestinationBounds(), direction, tx); + sendOnPipTransitionFinished(direction); + if (direction == TRANSITION_DIRECTION_TO_PIP) { + // TODO (b//169221267): Add jank listener for transactions without buffer + // updates. + //InteractionJankMonitor.getInstance().end( + // InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP); + } + } + + @Override + public void onPipAnimationCancel(TaskInfo taskInfo, + PipAnimationController.PipTransitionAnimator animator) { + sendOnPipTransitionCancelled(animator.getTransitionDirection()); + } + }; + + /** + * Called when transition is about to finish. This is usually for performing tasks such as + * applying WindowContainerTransaction to finalize the PiP bounds and send to the framework. + */ + public void onFinishResize(TaskInfo taskInfo, Rect destinationBounds, + @PipAnimationController.TransitionDirection int direction, + SurfaceControl.Transaction tx) { + } + + public PipTransitionController(PipBoundsState pipBoundsState, + PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, + PipAnimationController pipAnimationController, Transitions transitions, + @android.annotation.NonNull ShellTaskOrganizer shellTaskOrganizer) { + mPipBoundsState = pipBoundsState; + mPipMenuController = pipMenuController; + mShellTaskOrganizer = shellTaskOrganizer; + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipAnimationController = pipAnimationController; + mMainHandler = new Handler(Looper.getMainLooper()); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + transitions.addHandler(this); + } + } + + /** + * Registers {@link PipTransitionCallback} to receive transition callbacks. + */ + public void registerPipTransitionCallback(PipTransitionCallback callback) { + mPipTransitionCallbacks.add(callback); + } + + protected void sendOnPipTransitionStarted( + @PipAnimationController.TransitionDirection int direction) { + final Rect pipBounds = mPipBoundsState.getBounds(); + for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { + final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); + callback.onPipTransitionStarted(direction, pipBounds); + } + } + + protected void sendOnPipTransitionFinished( + @PipAnimationController.TransitionDirection int direction) { + for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { + final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); + callback.onPipTransitionFinished(direction); + } + } + + protected void sendOnPipTransitionCancelled( + @PipAnimationController.TransitionDirection int direction) { + for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { + final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); + callback.onPipTransitionCanceled(direction); + } + } + + /** + * The windowing mode to restore to when resizing out of PIP direction. Defaults to undefined + * and can be overridden to restore to an alternate windowing mode. + */ + public int getOutPipWindowingMode() { + // By default, simply reset the windowing mode to undefined. + return WINDOWING_MODE_UNDEFINED; + } + + protected void setBoundsStateForEntry(ComponentName componentName, + PictureInPictureParams params, + ActivityInfo activityInfo) { + mPipBoundsState.setBoundsStateForEntry(componentName, + mPipBoundsAlgorithm.getAspectRatioOrDefault(params), + mPipBoundsAlgorithm.getMinimalSize(activityInfo)); + } + + /** + * Callback interface for PiP transitions (both from and to PiP mode) + */ + public interface PipTransitionCallback { + /** + * Callback when the pip transition is started. + */ + void onPipTransitionStarted(int direction, Rect pipBounds); + + /** + * Callback when the pip transition is finished. + */ + void onPipTransitionFinished(int direction); + + /** + * Callback when the pip transition is cancelled. + */ + void onPipTransitionCanceled(int direction); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index 4b118f121767..ae5300502993 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -198,8 +198,7 @@ public class PhonePipMenuController implements PipMenuController { * {@code null}), it will get the leash that the WindowlessWM has assigned to it. */ public SurfaceControl getSurfaceControl() { - SurfaceControl sf = mPipMenuView.getWindowSurfaceControl(); - return sf != null ? sf : mSystemWindows.getViewSurface(mPipMenuView); + return mSystemWindows.getViewSurface(mPipMenuView); } /** @@ -212,8 +211,8 @@ public class PhonePipMenuController implements PipMenuController { } @Nullable - Size getEstimatedMenuSize() { - return mPipMenuView == null ? null : mPipMenuView.getEstimatedMenuSize(); + Size getEstimatedMinMenuSize() { + return mPipMenuView == null ? null : mPipMenuView.getEstimatedMinMenuSize(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index c06f9d03cdf7..c3970e33d736 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -61,6 +61,7 @@ import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipUtils; import java.io.PrintWriter; @@ -69,7 +70,7 @@ import java.util.function.Consumer; /** * Manages the picture-in-picture (PIP) UI and states for Phones. */ -public class PipController implements PipTaskOrganizer.PipTransitionCallback { +public class PipController implements PipTransitionController.PipTransitionCallback { private static final String TAG = "PipController"; private Context mContext; @@ -82,6 +83,7 @@ public class PipController implements PipTaskOrganizer.PipTransitionCallback { private PipBoundsAlgorithm mPipBoundsAlgorithm; private PipBoundsState mPipBoundsState; private PipTouchHandler mTouchHandler; + private PipTransitionController mPipTransitionController; protected final PipImpl mImpl = new PipImpl(); private final Rect mTmpInsetBounds = new Rect(); @@ -214,7 +216,6 @@ public class PipController implements PipTaskOrganizer.PipTransitionCallback { } } - /** * Instantiates {@link PipController}, returns {@code null} if the feature not supported. */ @@ -223,7 +224,8 @@ public class PipController implements PipTaskOrganizer.PipTransitionCallback { PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, PipBoundsState pipBoundsState, PipMediaController pipMediaController, PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, - PipTouchHandler pipTouchHandler, WindowManagerShellWrapper windowManagerShellWrapper, + PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, + WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, ShellExecutor mainExecutor) { if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { Slog.w(TAG, "Device doesn't support Pip feature"); @@ -232,7 +234,8 @@ public class PipController implements PipTaskOrganizer.PipTransitionCallback { return new PipController(context, displayController, pipAppOpsListener, pipBoundsAlgorithm, pipBoundsState, pipMediaController, phonePipMenuController, pipTaskOrganizer, - pipTouchHandler, windowManagerShellWrapper, taskStackListener, mainExecutor) + pipTouchHandler, pipTransitionController, windowManagerShellWrapper, + taskStackListener, mainExecutor) .mImpl; } @@ -245,6 +248,7 @@ public class PipController implements PipTaskOrganizer.PipTransitionCallback { PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, PipTouchHandler pipTouchHandler, + PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, ShellExecutor mainExecutor @@ -266,9 +270,10 @@ public class PipController implements PipTaskOrganizer.PipTransitionCallback { mMenuController = phonePipMenuController; mTouchHandler = pipTouchHandler; mAppOpsListener = pipAppOpsListener; + mPipTransitionController = pipTransitionController; mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(), INPUT_CONSUMER_PIP, mainExecutor); - mPipTaskOrganizer.registerPipTransitionCallback(this); + mPipTransitionController.registerPipTransitionCallback(this); mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> { mPipBoundsState.setDisplayId(displayId); onDisplayChanged(displayController.getDisplayLayout(displayId), @@ -489,7 +494,7 @@ public class PipController implements PipTaskOrganizer.PipTransitionCallback { } @Override - public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) { + public void onPipTransitionStarted(int direction, Rect pipBounds) { if (isOutPipDirection(direction)) { // Exiting PIP, save the reentry state to restore to when re-entering. saveReentryState(pipBounds); @@ -514,12 +519,12 @@ public class PipController implements PipTaskOrganizer.PipTransitionCallback { } @Override - public void onPipTransitionFinished(ComponentName activity, int direction) { + public void onPipTransitionFinished(int direction) { onPipTransitionFinishedOrCanceled(direction); } @Override - public void onPipTransitionCanceled(ComponentName activity, int direction) { + public void onPipTransitionCanceled(int direction) { onPipTransitionFinishedOrCanceled(direction); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java index 6d12752d9218..3eeba6eb5366 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java @@ -31,7 +31,6 @@ public class PipMenuIconsAlgorithm { private static final String TAG = "PipMenuIconsAlgorithm"; - private boolean mFinishedLayout = false; protected ViewGroup mViewRoot; protected ViewGroup mTopEndContainer; protected View mDragHandle; @@ -51,27 +50,16 @@ public class PipMenuIconsAlgorithm { mDragHandle = dragHandle; mSettingsButton = settingsButton; mDismissButton = dismissButton; + + bindInitialViewState(); } /** * Updates the position of the drag handle based on where the PIP window is on the screen. */ public void onBoundsChanged(Rect bounds) { - if (mViewRoot == null || mTopEndContainer == null || mDragHandle == null - || mSettingsButton == null || mDismissButton == null) { - Log.e(TAG, "One if the required views is null."); - return; - } - - //We only need to calculate the layout once since it does not change. - if (!mFinishedLayout) { - mTopEndContainer.removeView(mSettingsButton); - mViewRoot.addView(mSettingsButton); - - setLayoutGravity(mDragHandle, Gravity.START | Gravity.TOP); - setLayoutGravity(mSettingsButton, Gravity.START | Gravity.TOP); - mFinishedLayout = true; - } + // On phones, the menu icons are always static and will never move based on the PIP window + // position. No need to do anything here. } /** @@ -84,4 +72,22 @@ public class PipMenuIconsAlgorithm { v.setLayoutParams(params); } } + + /** Calculate the initial state of the menu icons. Called when the menu is first created. */ + private void bindInitialViewState() { + if (mViewRoot == null || mTopEndContainer == null || mDragHandle == null + || mSettingsButton == null || mDismissButton == null) { + Log.e(TAG, "One of the required views is null."); + return; + } + // The menu view layout starts out with the settings button aligned at the top|end of the + // view group next to the dismiss button. On phones, the settings button should be aligned + // to the top|start of the view, so move it to parent view group to then align it to the + // top|start of the menu. + mTopEndContainer.removeView(mSettingsButton); + mViewRoot.addView(mSettingsButton); + + setLayoutGravity(mDragHandle, Gravity.START | Gravity.TOP); + setLayoutGravity(mSettingsButton, Gravity.START | Gravity.TOP); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java index 48942b604a8d..962c4672644a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java @@ -231,7 +231,6 @@ public class PipMenuView extends FrameLayout { && (mMenuState == MENU_STATE_FULL || menuState == MENU_STATE_FULL); mAllowTouches = !disallowTouchesUntilAnimationEnd; cancelDelayedHide(); - updateActionViews(stackBounds); if (mMenuContainerAnimator != null) { mMenuContainerAnimator.cancel(); } @@ -280,6 +279,7 @@ public class PipMenuView extends FrameLayout { setVisibility(VISIBLE); mMenuContainerAnimator.start(); } + updateActionViews(stackBounds); } else { // If we are already visible, then just start the delayed dismiss and unregister any // existing input consumers from the previous drag @@ -367,15 +367,17 @@ public class PipMenuView extends FrameLayout { } /** - * @return estimated {@link Size} for which the width is based on number of actions and - * height based on the height of expand button + top and bottom action bar. + * @return Estimated minimum {@link Size} to hold the actions. + * See also {@link #updateActionViews(Rect)} */ - Size getEstimatedMenuSize() { - final int pipActionSize = mContext.getResources().getDimensionPixelSize( - R.dimen.pip_action_size); - final int width = mActions.size() * pipActionSize; - final int height = pipActionSize * 2 + mContext.getResources().getDimensionPixelSize( - R.dimen.pip_expand_action_size); + Size getEstimatedMinMenuSize() { + final int pipActionSize = getResources().getDimensionPixelSize(R.dimen.pip_action_size); + // the minimum width would be (2 * pipActionSize) since we have settings and dismiss button + // on the top action container. + final int width = Math.max(2, mActions.size()) * pipActionSize; + final int height = getResources().getDimensionPixelSize(R.dimen.pip_expand_action_size) + + getResources().getDimensionPixelSize(R.dimen.pip_action_padding) + + getResources().getDimensionPixelSize(R.dimen.pip_expand_container_edge_margin); return new Size(width, height); } @@ -393,7 +395,7 @@ public class PipMenuView extends FrameLayout { return true; }); - if (mActions.isEmpty() || mMenuState == MENU_STATE_CLOSE) { + if (mActions.isEmpty() || mMenuState == MENU_STATE_CLOSE || mMenuState == MENU_STATE_NONE) { actionsContainer.setVisibility(View.INVISIBLE); } else { actionsContainer.setVisibility(View.VISIBLE); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java index 4df7cef12f63..b19dcae2def8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -22,12 +22,10 @@ import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT; import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.ComponentName; import android.content.Context; import android.graphics.PointF; import android.graphics.Rect; import android.os.Debug; -import android.os.Handler; import android.os.Looper; import android.util.Log; import android.view.Choreographer; @@ -45,6 +43,7 @@ import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipTransitionController; import java.util.function.Consumer; @@ -62,6 +61,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, private static final int SHRINK_STACK_FROM_MENU_DURATION = 250; private static final int EXPAND_STACK_TO_MENU_DURATION = 250; + private static final int UNSTASH_DURATION = 250; private static final int LEAVE_PIP_DURATION = 300; private static final int SHIFT_DURATION = 300; @@ -152,13 +152,13 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, */ private Runnable mPostPipTransitionCallback; - private final PipTaskOrganizer.PipTransitionCallback mPipTransitionCallback = - new PipTaskOrganizer.PipTransitionCallback() { + private final PipTransitionController.PipTransitionCallback mPipTransitionCallback = + new PipTransitionController.PipTransitionCallback() { @Override - public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) {} + public void onPipTransitionStarted(int direction, Rect pipBounds) {} @Override - public void onPipTransitionFinished(ComponentName activity, int direction) { + public void onPipTransitionFinished(int direction) { if (mPostPipTransitionCallback != null) { mPostPipTransitionCallback.run(); mPostPipTransitionCallback = null; @@ -166,20 +166,20 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, } @Override - public void onPipTransitionCanceled(ComponentName activity, int direction) {} + public void onPipTransitionCanceled(int direction) {} }; public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController, - PipSnapAlgorithm snapAlgorithm, FloatingContentCoordinator floatingContentCoordinator, - ShellExecutor mainExecutor) { + PipSnapAlgorithm snapAlgorithm, PipTransitionController pipTransitionController, + FloatingContentCoordinator floatingContentCoordinator, ShellExecutor mainExecutor) { mContext = context; mPipTaskOrganizer = pipTaskOrganizer; mPipBoundsState = pipBoundsState; mMenuController = menuController; mSnapAlgorithm = snapAlgorithm; mFloatingContentCoordinator = floatingContentCoordinator; - mPipTaskOrganizer.registerPipTransitionCallback(mPipTransitionCallback); + pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback); mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance( mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); @@ -482,6 +482,13 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, } /** + * Animates the PiP from stashed state into un-stashed, popping it out from the edge. + */ + void animateToUnStashedBounds(Rect unstashedBounds) { + resizeAndAnimatePipUnchecked(unstashedBounds, UNSTASH_DURATION); + } + + /** * Animates the PiP to offset it from the IME or shelf. */ @VisibleForTesting diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java index 8fb358ad74d1..1ef9ffa494f4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java @@ -64,6 +64,7 @@ public class PipResizeGestureHandler { private static final String TAG = "PipResizeGestureHandler"; private static final int PINCH_RESIZE_SNAP_DURATION = 250; private static final int PINCH_RESIZE_MAX_ANGLE_ROTATION = 45; + private static final float PINCH_RESIZE_AUTO_MAX_RATIO = 0.9f; private final Context mContext; private final PipBoundsAlgorithm mPipBoundsAlgorithm; @@ -106,6 +107,7 @@ public class PipResizeGestureHandler { private boolean mThresholdCrossed0; private boolean mThresholdCrossed1; private boolean mUsingPinchToZoom = false; + private float mAngle = 0; int mFirstIndex = -1; int mSecondIndex = -1; @@ -419,18 +421,25 @@ public class PipResizeGestureHandler { float down1X = mDownSecondaryPoint.x; float down1Y = mDownSecondaryPoint.y; - // Top right + Bottom left pinch to zoom. - if ((down0X > focusX && down0Y < focusY && down1X < focusX && down1Y > focusY) - || (down1X > focusX && down1Y < focusY - && down0X < focusX && down0Y > focusY)) { + if (down0X > focusX && down0Y < focusY && down1X < focusX && down1Y > focusY) { + // Top right + Bottom left pinch to zoom. mAngle = calculateRotationAngle(mLastResizeBounds.centerX(), mLastResizeBounds.centerY(), x0, y0, x1, y1, true); - } else if ((down0X < focusX && down0Y < focusY - && down1X > focusX && down1Y > focusY) - || (down1X < focusX && down1Y < focusY - && down0X > focusX && down0Y > focusY)) { + } else if (down1X > focusX && down1Y < focusY + && down0X < focusX && down0Y > focusY) { + // Top right + Bottom left pinch to zoom. + mAngle = calculateRotationAngle(mLastResizeBounds.centerX(), + mLastResizeBounds.centerY(), x1, y1, x0, y0, true); + } else if (down0X < focusX && down0Y < focusY + && down1X > focusX && down1Y > focusY) { + // Top left + bottom right pinch to zoom. mAngle = calculateRotationAngle(mLastResizeBounds.centerX(), mLastResizeBounds.centerY(), x0, y0, x1, y1, false); + } else if (down1X < focusX && down1Y < focusY + && down0X > focusX && down0Y > focusY) { + // Top left + bottom right pinch to zoom. + mAngle = calculateRotationAngle(mLastResizeBounds.centerX(), + mLastResizeBounds.centerY(), x1, y1, x0, y0, false); } mLastResizeBounds.set(PipPinchResizingAlgorithm.pinchResize(x0, y0, x1, y1, @@ -444,21 +453,26 @@ public class PipResizeGestureHandler { } } - private float mAngle = 0; - - private float calculateRotationAngle(int focusX, int focusY, float x0, float y0, - float x1, float y1, boolean positive) { + private float calculateRotationAngle(int pivotX, int pivotY, float topX, float topY, + float bottomX, float bottomY, boolean positive) { // The base angle is the angle formed by taking the angle between the center horizontal // and one of the corners. - double baseAngle = Math.toDegrees(Math.atan2(Math.abs(mLastResizeBounds.top - focusY), - Math.abs(mLastResizeBounds.right - focusX))); + double baseAngle = Math.toDegrees(Math.atan2(Math.abs(mLastResizeBounds.top - pivotY), + Math.abs(mLastResizeBounds.right - pivotX))); + double angle0 = mThresholdCrossed0 - ? Math.toDegrees(Math.atan2(Math.abs(y0 - focusY), Math.abs(x0 - focusX))) - : baseAngle; - double angle1 = mThresholdCrossed1 - ? Math.toDegrees(Math.atan2(Math.abs(y1 - focusY), Math.abs(x1 - focusX))) - : baseAngle; + ? Math.toDegrees(Math.atan2(pivotY - topY, topX - pivotX)) : baseAngle; + double angle1 = mThresholdCrossed0 + ? Math.toDegrees(Math.atan2(pivotY - bottomY, bottomX - pivotX)) : baseAngle; + + + if (positive) { + angle1 = angle1 < 0 ? 180 + angle1 : angle1 - 180; + } else { + angle0 = angle0 < 0 ? -angle0 - 180 : 180 - angle0; + angle1 = -angle1; + } // Calculate the percentage difference of [0, 90] compare to the base angle. double diff0 = (Math.max(0, Math.min(angle0, 90)) - baseAngle) / 90; @@ -539,8 +553,13 @@ public class PipResizeGestureHandler { // position correctly. Drag-resize does not need to move, so just finalize resize. if (mUsingPinchToZoom) { final Rect startBounds = new Rect(mLastResizeBounds); - mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, - mPipBoundsAlgorithm.getSnapFraction(mPipBoundsState.getBounds())); + // If user resize is pretty close to max size, just auto resize to max. + if (mLastResizeBounds.width() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.x + || mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) { + mLastResizeBounds.set(0, 0, mMaxSize.x, mMaxSize.y); + } + final float snapFraction = mPipBoundsAlgorithm.getSnapFraction(mLastResizeBounds); + mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction); mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds, PINCH_RESIZE_SNAP_DURATION, -mAngle, callback); } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index 128d13c2ce2e..e69c6f2f47bc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -17,7 +17,9 @@ package com.android.wm.shell.pip.phone; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASHING; +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASH_MINIMUM_VELOCITY_THRESHOLD; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; +import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE; import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_CLOSE; import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL; import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE; @@ -30,7 +32,6 @@ import android.content.res.Resources; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; -import android.os.Handler; import android.provider.DeviceConfig; import android.util.Log; import android.util.Size; @@ -50,6 +51,7 @@ import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipUiEventLogger; import java.io.PrintWriter; @@ -62,9 +64,8 @@ import java.util.function.Consumer; */ public class PipTouchHandler { private static final String TAG = "PipTouchHandler"; - - private static final float STASH_MINIMUM_VELOCITY_X = 3000.f; private static final float MINIMUM_SIZE_PERCENT = 0.4f; + private static final float DEFAULT_STASH_VELOCITY_THRESHOLD = 18000.f; // Allow PIP to resize to a slightly bigger state upon touch private final boolean mEnableResize; @@ -87,6 +88,8 @@ public class PipTouchHandler { */ private boolean mEnableStash = true; + private float mStashVelocityThreshold; + // The reference inset bounds, used to determine the dismiss fraction private final Rect mInsetBounds = new Rect(); private int mExpandedShortestEdgeSize; @@ -154,6 +157,7 @@ public class PipTouchHandler { PipBoundsAlgorithm pipBoundsAlgorithm, @NonNull PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, + PipTransitionController pipTransitionController, FloatingContentCoordinator floatingContentCoordinator, PipUiEventLogger pipUiEventLogger, ShellExecutor mainExecutor) { @@ -166,7 +170,7 @@ public class PipTouchHandler { mMenuController.addListener(new PipMenuListener()); mGesture = new DefaultPipTouchGesture(); mMotionHelper = new PipMotionHelper(mContext, pipBoundsState, pipTaskOrganizer, - mMenuController, mPipBoundsAlgorithm.getSnapAlgorithm(), + mMenuController, mPipBoundsAlgorithm.getSnapAlgorithm(), pipTransitionController, floatingContentCoordinator, mainExecutor); mPipResizeGestureHandler = new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState, @@ -176,9 +180,17 @@ public class PipTouchHandler { mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger, mMotionHelper, mainExecutor); mTouchState = new PipTouchState(ViewConfiguration.get(context), - () -> mMenuController.showMenuWithPossibleDelay(MENU_STATE_FULL, - mPipBoundsState.getBounds(), true /* allowMenuTimeout */, willResizeMenu(), - shouldShowResizeHandle()), + () -> { + if (mPipBoundsState.isStashed()) { + animateToUnStashedState(); + mPipBoundsState.setStashed(STASH_TYPE_NONE); + } else { + mMenuController.showMenuWithPossibleDelay(MENU_STATE_FULL, + mPipBoundsState.getBounds(), true /* allowMenuTimeout */, + willResizeMenu(), + shouldShowResizeHandle()); + } + }, menuController::hideMenu, mainExecutor); @@ -205,6 +217,19 @@ public class PipTouchHandler { PIP_STASHING, /* defaultValue = */ true); } }); + mStashVelocityThreshold = DeviceConfig.getFloat( + DeviceConfig.NAMESPACE_SYSTEMUI, + PIP_STASH_MINIMUM_VELOCITY_THRESHOLD, + DEFAULT_STASH_VELOCITY_THRESHOLD); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + mainExecutor, + properties -> { + if (properties.getKeyset().contains(PIP_STASH_MINIMUM_VELOCITY_THRESHOLD)) { + mStashVelocityThreshold = properties.getFloat( + PIP_STASH_MINIMUM_VELOCITY_THRESHOLD, + DEFAULT_STASH_VELOCITY_THRESHOLD); + } + }); } private void reloadResources() { @@ -710,6 +735,17 @@ public class PipTouchHandler { mSavedSnapFraction = -1f; } + private void animateToUnStashedState() { + final Rect pipBounds = mPipBoundsState.getBounds(); + final boolean onLeftEdge = pipBounds.left < mPipBoundsState.getDisplayBounds().left; + final Rect unStashedBounds = new Rect(0, pipBounds.top, 0, pipBounds.bottom); + unStashedBounds.left = onLeftEdge ? mInsetBounds.left + : mInsetBounds.right - pipBounds.width(); + unStashedBounds.right = onLeftEdge ? mInsetBounds.left + pipBounds.width() + : mInsetBounds.right; + mMotionHelper.animateToUnStashedBounds(unStashedBounds); + } + /** * @return the motion helper. */ @@ -773,7 +809,7 @@ public class PipTouchHandler { } if (touchState.startedDragging()) { - mPipBoundsState.setStashed(PipBoundsState.STASH_TYPE_NONE); + mPipBoundsState.setStashed(STASH_TYPE_NONE); mSavedSnapFraction = -1f; mPipDismissTargetHandler.showDismissTargetMaybe(); } @@ -826,14 +862,8 @@ public class PipTouchHandler { // Reset the touch state on up before the fling settles mTouchState.reset(); - // If user flings the PIP window above the minimum velocity, stash PIP. - // Only allow stashing to the edge if the user starts dragging the PIP from that - // edge. if (mEnableStash && !mPipBoundsState.isStashed() - && ((vel.x > STASH_MINIMUM_VELOCITY_X - && mDownSavedFraction > 1f && mDownSavedFraction < 2f) - || (vel.x < -STASH_MINIMUM_VELOCITY_X - && mDownSavedFraction > 3f && mDownSavedFraction < 4f))) { + && shouldStash(vel, getPossiblyMotionBounds())) { mMotionHelper.stashToEdge(vel.x, this::stashEndAction /* endAction */); } else { mMotionHelper.flingToSnapTarget(vel.x, vel.y, @@ -847,24 +877,29 @@ public class PipTouchHandler { && mPipBoundsState.getBounds().height() < mPipBoundsState.getMaxSize().y; if (toExpand) { + mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); animateToMaximizedState(null); } else { - animateToMinimizedState(); + animateToUnexpandedState(getUserResizeBounds()); } - mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); } else { // Expand to fullscreen if this is a double tap // the PiP should be frozen until the transition ends setTouchEnabled(false); mMotionHelper.expandLeavePip(); } - } else if (mMenuState != MENU_STATE_FULL && !mPipBoundsState.isStashed()) { + } else if (mMenuState != MENU_STATE_FULL) { if (!mTouchState.isWaitingForDoubleTap()) { - // User has stalled long enough for this not to be a drag or a double tap, just - // expand the menu - mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), - true /* allowMenuTimeout */, willResizeMenu(), - shouldShowResizeHandle()); + if (mPipBoundsState.isStashed()) { + animateToUnStashedState(); + mPipBoundsState.setStashed(STASH_TYPE_NONE); + } else { + // User has stalled long enough for this not to be a drag or a double tap, + // just expand the menu + mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), + shouldShowResizeHandle()); + } } else { // Next touch event _may_ be the second tap for the double-tap, schedule a // fallback runnable to trigger the menu if no touch event occurs before the @@ -895,6 +930,26 @@ public class PipTouchHandler { mPipExclusionBoundsChangeListener.get().accept(new Rect()); } } + + private boolean shouldStash(PointF vel, Rect motionBounds) { + // If user flings the PIP window above the minimum velocity, stash PIP. + // Only allow stashing to the edge if the user starts dragging the PIP from the + // opposite edge. + final boolean stashFromFlingToEdge = ((vel.x < -mStashVelocityThreshold + && mDownSavedFraction > 1f && mDownSavedFraction < 2f) + || (vel.x > mStashVelocityThreshold + && mDownSavedFraction > 3f && mDownSavedFraction < 4f)); + + // If User releases the PIP window while it's out of the display bounds, put + // PIP into stashed mode. + final int offset = motionBounds.width() / 2; + final boolean stashFromDroppingOnEdge = + (motionBounds.right > mPipBoundsState.getDisplayBounds().right + offset + || motionBounds.left + < mPipBoundsState.getDisplayBounds().left - offset); + + return stashFromFlingToEdge || stashFromDroppingOnEdge; + } } void setPipExclusionBoundsChangeListener(Consumer<Rect> pipExclusionBoundsChangeListener) { @@ -932,14 +987,14 @@ public class PipTouchHandler { if (!mEnableResize) { return false; } - final Size estimatedMenuSize = mMenuController.getEstimatedMenuSize(); - if (estimatedMenuSize == null) { + final Size estimatedMinMenuSize = mMenuController.getEstimatedMinMenuSize(); + if (estimatedMinMenuSize == null) { Log.wtf(TAG, "Failed to get estimated menu size"); return false; } final Rect currentBounds = mPipBoundsState.getBounds(); - return currentBounds.width() < estimatedMenuSize.getWidth() - || currentBounds.height() < estimatedMenuSize.getHeight(); + return currentBounds.width() < estimatedMinMenuSize.getWidth() + || currentBounds.height() < estimatedMinMenuSize.getHeight(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index 75fc9f5a4ecf..56f183fd7303 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -46,6 +46,7 @@ import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipTransitionController; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -53,7 +54,7 @@ import java.lang.annotation.RetentionPolicy; /** * Manages the picture-in-picture (PIP) UI and states. */ -public class TvPipController implements PipTaskOrganizer.PipTransitionCallback, +public class TvPipController implements PipTransitionController.PipTransitionCallback, TvPipMenuController.Delegate, TvPipNotificationController.Delegate { private static final String TAG = "TvPipController"; static final boolean DEBUG = true; @@ -105,6 +106,7 @@ public class TvPipController implements PipTaskOrganizer.PipTransitionCallback, PipBoundsState pipBoundsState, PipBoundsAlgorithm pipBoundsAlgorithm, PipTaskOrganizer pipTaskOrganizer, + PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, TvPipNotificationController pipNotificationController, @@ -116,6 +118,7 @@ public class TvPipController implements PipTaskOrganizer.PipTransitionCallback, pipBoundsState, pipBoundsAlgorithm, pipTaskOrganizer, + pipTransitionController, tvPipMenuController, pipMediaController, pipNotificationController, @@ -129,6 +132,7 @@ public class TvPipController implements PipTaskOrganizer.PipTransitionCallback, PipBoundsState pipBoundsState, PipBoundsAlgorithm pipBoundsAlgorithm, PipTaskOrganizer pipTaskOrganizer, + PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, TvPipNotificationController pipNotificationController, @@ -152,7 +156,7 @@ public class TvPipController implements PipTaskOrganizer.PipTransitionCallback, mTvPipMenuController.setDelegate(this); mPipTaskOrganizer = pipTaskOrganizer; - mPipTaskOrganizer.registerPipTransitionCallback(this); + pipTransitionController.registerPipTransitionCallback(this); loadConfigurations(); @@ -302,17 +306,17 @@ public class TvPipController implements PipTaskOrganizer.PipTransitionCallback, } @Override - public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) { + public void onPipTransitionStarted(int direction, Rect pipBounds) { if (DEBUG) Log.d(TAG, "onPipTransition_Started(), state=" + stateToName(mState)); } @Override - public void onPipTransitionCanceled(ComponentName activity, int direction) { + public void onPipTransitionCanceled(int direction) { if (DEBUG) Log.d(TAG, "onPipTransition_Canceled(), state=" + stateToName(mState)); } @Override - public void onPipTransitionFinished(ComponentName activity, int direction) { + public void onPipTransitionFinished(int direction) { if (DEBUG) Log.d(TAG, "onPipTransition_Finished(), state=" + stateToName(mState)); if (mState == STATE_PIP_MENU) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java new file mode 100644 index 000000000000..b7caf72641a3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021 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.wm.shell.pip.tv; + +import android.app.TaskInfo; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipBoundsAlgorithm; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipMenuController; +import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.transition.Transitions; + +/** + * PiP Transition for TV. + * TODO: Implement animation once TV is using Transitions. + */ +public class TvPipTransition extends PipTransitionController { + public TvPipTransition(PipBoundsState pipBoundsState, + PipMenuController pipMenuController, + PipBoundsAlgorithm pipBoundsAlgorithm, + PipAnimationController pipAnimationController, + Transitions transitions, + @NonNull ShellTaskOrganizer shellTaskOrganizer) { + super(pipBoundsState, pipMenuController, pipBoundsAlgorithm, pipAnimationController, + transitions, shellTaskOrganizer); + } + + @Override + public void onFinishResize(TaskInfo taskInfo, Rect destinationBounds, int direction, + SurfaceControl.Transaction tx) { + + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + return false; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java new file mode 100644 index 000000000000..9094d7de8d63 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2021 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.wm.shell.sizecompatui; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.RippleDrawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.PopupWindow; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.R; + +/** Button to restart the size compat activity. */ +public class SizeCompatRestartButton extends FrameLayout implements View.OnClickListener, + View.OnLongClickListener { + + private SizeCompatUILayout mLayout; + private ImageButton mRestartButton; + @VisibleForTesting + PopupWindow mShowingHint; + private WindowManager.LayoutParams mWinParams; + + public SizeCompatRestartButton(@NonNull Context context) { + super(context); + } + + public SizeCompatRestartButton(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public SizeCompatRestartButton(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SizeCompatRestartButton(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + void inject(SizeCompatUILayout layout) { + mLayout = layout; + mWinParams = layout.getWindowLayoutParams(); + } + + void remove() { + dismissHint(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mRestartButton = findViewById(R.id.size_compat_restart_button); + final ColorStateList color = ColorStateList.valueOf(Color.LTGRAY); + final GradientDrawable mask = new GradientDrawable(); + mask.setShape(GradientDrawable.OVAL); + mask.setColor(color); + mRestartButton.setBackground(new RippleDrawable(color, null /* content */, mask)); + mRestartButton.setOnClickListener(this); + mRestartButton.setOnLongClickListener(this); + } + + @Override + public void onClick(View v) { + mLayout.onRestartButtonClicked(); + } + + @Override + public boolean onLongClick(View v) { + showHint(); + return true; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mLayout.mShouldShowHint) { + mLayout.mShouldShowHint = false; + showHint(); + } + } + + @Override + public void setVisibility(@Visibility int visibility) { + if (visibility == View.GONE && mShowingHint != null) { + // Also dismiss the popup. + dismissHint(); + } + super.setVisibility(visibility); + } + + @Override + public void setLayoutDirection(int layoutDirection) { + final int gravity = SizeCompatUILayout.getGravity(layoutDirection); + if (mWinParams.gravity != gravity) { + mWinParams.gravity = gravity; + getContext().getSystemService(WindowManager.class).updateViewLayout(this, + mWinParams); + } + super.setLayoutDirection(layoutDirection); + } + + void showHint() { + if (mShowingHint != null) { + return; + } + + // TODO: popup is not attached to the button surface. Need to handle this differently for + // non-fullscreen task. + final View popupView = LayoutInflater.from(getContext()).inflate( + R.layout.size_compat_mode_hint, null); + final PopupWindow popupWindow = new PopupWindow(popupView, + LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); + popupWindow.setWindowLayoutType(mWinParams.type); + popupWindow.setElevation(getResources().getDimension(R.dimen.bubble_elevation)); + popupWindow.setAnimationStyle(android.R.style.Animation_InputMethod); + popupWindow.setClippingEnabled(false); + popupWindow.setOnDismissListener(() -> mShowingHint = null); + mShowingHint = popupWindow; + + final Button gotItButton = popupView.findViewById(R.id.got_it); + gotItButton.setBackground(new RippleDrawable(ColorStateList.valueOf(Color.LTGRAY), + null /* content */, null /* mask */)); + gotItButton.setOnClickListener(view -> dismissHint()); + popupWindow.showAtLocation(mRestartButton, mWinParams.gravity, mLayout.mPopupOffsetX, + mLayout.mPopupOffsetY); + } + + void dismissHint() { + if (mShowingHint != null) { + mShowingHint.dismiss(); + mShowingHint = null; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java new file mode 100644 index 000000000000..a3880f497ff3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2021 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.wm.shell.sizecompatui; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Configuration; +import android.hardware.display.DisplayManager; +import android.os.IBinder; +import android.util.ArraySet; +import android.util.Log; +import android.util.SparseArray; +import android.view.Display; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +/** + * Controls to show/update restart-activity buttons on Tasks based on whether the foreground + * activities are in size compatibility mode. + */ +public class SizeCompatUIController implements DisplayController.OnDisplaysChangedListener, + DisplayImeController.ImePositionProcessor { + private static final String TAG = "SizeCompatUIController"; + + /** Whether the IME is shown on display id. */ + private final Set<Integer> mDisplaysWithIme = new ArraySet<>(1); + + /** The showing buttons by task id. */ + private final SparseArray<SizeCompatUILayout> mActiveLayouts = new SparseArray<>(0); + + /** Avoid creating display context frequently for non-default display. */ + private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0); + + private final Context mContext; + private final DisplayController mDisplayController; + private final DisplayImeController mImeController; + private final SyncTransactionQueue mSyncQueue; + + /** Only show once automatically in the process life. */ + private boolean mHasShownHint; + + public SizeCompatUIController(Context context, + DisplayController displayController, + DisplayImeController imeController, + SyncTransactionQueue syncQueue) { + mContext = context; + mDisplayController = displayController; + mImeController = imeController; + mSyncQueue = syncQueue; + mDisplayController.addDisplayWindowListener(this); + mImeController.addPositionProcessor(this); + } + + /** + * Called when the Task info changed. Creates and updates the restart button if there is an + * activity in size compat, or removes the restart button if there is no size compat activity. + * + * @param displayId display the task and activity are in. + * @param taskId task the activity is in. + * @param taskConfig task config to place the restart button with. + * @param sizeCompatActivity the size compat activity in the task. Can be {@code null} if the + * top activity in this Task is not in size compat. + * @param taskListener listener to handle the Task Surface placement. + */ + public void onSizeCompatInfoChanged(int displayId, int taskId, + @Nullable Configuration taskConfig, @Nullable IBinder sizeCompatActivity, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { + if (taskConfig == null || sizeCompatActivity == null || taskListener == null) { + // Null token means the current foreground activity is not in size compatibility mode. + removeLayout(taskId); + } else if (mActiveLayouts.contains(taskId)) { + // Button already exists, update the button layout. + updateLayout(taskId, taskConfig, sizeCompatActivity, taskListener); + } else { + // Create a new restart button. + createLayout(displayId, taskId, taskConfig, sizeCompatActivity, taskListener); + } + } + + @Override + public void onDisplayRemoved(int displayId) { + mDisplayContextCache.remove(displayId); + + // Remove all buttons on the removed display. + final List<Integer> toRemoveTaskIds = new ArrayList<>(); + forAllLayoutsOnDisplay(displayId, layout -> toRemoveTaskIds.add(layout.getTaskId())); + for (int i = toRemoveTaskIds.size() - 1; i >= 0; i--) { + removeLayout(toRemoveTaskIds.get(i)); + } + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(displayId); + forAllLayoutsOnDisplay(displayId, layout -> layout.updateDisplayLayout(displayLayout)); + } + + @Override + public void onImeVisibilityChanged(int displayId, boolean isShowing) { + if (isShowing) { + mDisplaysWithIme.add(displayId); + } else { + mDisplaysWithIme.remove(displayId); + } + + // Hide the button when input method is showing. + forAllLayoutsOnDisplay(displayId, layout -> layout.updateImeVisibility(isShowing)); + } + + private boolean isImeShowingOnDisplay(int displayId) { + return mDisplaysWithIme.contains(displayId); + } + + private void createLayout(int displayId, int taskId, Configuration taskConfig, + IBinder activityToken, ShellTaskOrganizer.TaskListener taskListener) { + final Context context = getOrCreateDisplayContext(displayId); + if (context == null) { + Log.e(TAG, "Cannot get context for display " + displayId); + return; + } + + final SizeCompatUILayout layout = createLayout(context, displayId, taskId, taskConfig, + activityToken, taskListener); + mActiveLayouts.put(taskId, layout); + layout.createSizeCompatButton(isImeShowingOnDisplay(displayId)); + } + + @VisibleForTesting + SizeCompatUILayout createLayout(Context context, int displayId, int taskId, + Configuration taskConfig, IBinder activityToken, + ShellTaskOrganizer.TaskListener taskListener) { + final SizeCompatUILayout layout = new SizeCompatUILayout(mSyncQueue, context, taskConfig, + taskId, activityToken, taskListener, mDisplayController.getDisplayLayout(displayId), + mHasShownHint); + // Only show hint for the first time. + mHasShownHint = true; + return layout; + } + + private void updateLayout(int taskId, Configuration taskConfig, + IBinder sizeCompatActivity, + ShellTaskOrganizer.TaskListener taskListener) { + final SizeCompatUILayout layout = mActiveLayouts.get(taskId); + if (layout == null) { + return; + } + layout.updateSizeCompatInfo(taskConfig, sizeCompatActivity, taskListener, + isImeShowingOnDisplay(layout.getDisplayId())); + } + + private void removeLayout(int taskId) { + final SizeCompatUILayout layout = mActiveLayouts.get(taskId); + if (layout != null) { + layout.release(); + mActiveLayouts.remove(taskId); + } + } + + private Context getOrCreateDisplayContext(int displayId) { + if (displayId == Display.DEFAULT_DISPLAY) { + return mContext; + } + Context context = null; + final WeakReference<Context> ref = mDisplayContextCache.get(displayId); + if (ref != null) { + context = ref.get(); + } + if (context == null) { + Display display = mContext.getSystemService(DisplayManager.class).getDisplay(displayId); + if (display != null) { + context = mContext.createDisplayContext(display); + mDisplayContextCache.put(displayId, new WeakReference<>(context)); + } + } + return context; + } + + private void forAllLayoutsOnDisplay(int displayId, Consumer<SizeCompatUILayout> callback) { + for (int i = 0; i < mActiveLayouts.size(); i++) { + final int taskId = mActiveLayouts.keyAt(i); + final SizeCompatUILayout layout = mActiveLayouts.get(taskId); + if (layout != null && layout.getDisplayId() == displayId) { + callback.accept(layout); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java new file mode 100644 index 000000000000..5924b53f822c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2021 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.wm.shell.sizecompatui; + +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import android.annotation.Nullable; +import android.app.ActivityClient; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.Binder; +import android.os.IBinder; +import android.view.Gravity; +import android.view.SurfaceControl; +import android.view.View; +import android.view.WindowManager; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; + +/** + * Records and handles layout of size compat UI on a task with size compat activity. Helps to + * calculate proper bounds when configuration or button position changes. + */ +class SizeCompatUILayout { + private static final String TAG = "SizeCompatUILayout"; + + private final SyncTransactionQueue mSyncQueue; + private Context mContext; + private Configuration mTaskConfig; + private final int mDisplayId; + private final int mTaskId; + private IBinder mActivityToken; + private ShellTaskOrganizer.TaskListener mTaskListener; + private DisplayLayout mDisplayLayout; + @VisibleForTesting + final SizeCompatUIWindowManager mWindowManager; + + @VisibleForTesting + @Nullable + SizeCompatRestartButton mButton; + final int mButtonSize; + final int mPopupOffsetX; + final int mPopupOffsetY; + boolean mShouldShowHint; + + SizeCompatUILayout(SyncTransactionQueue syncQueue, Context context, Configuration taskConfig, + int taskId, IBinder activityToken, ShellTaskOrganizer.TaskListener taskListener, + DisplayLayout displayLayout, boolean hasShownHint) { + mSyncQueue = syncQueue; + mContext = context.createConfigurationContext(taskConfig); + mTaskConfig = taskConfig; + mDisplayId = mContext.getDisplayId(); + mTaskId = taskId; + mActivityToken = activityToken; + mTaskListener = taskListener; + mDisplayLayout = displayLayout; + mShouldShowHint = !hasShownHint; + mWindowManager = new SizeCompatUIWindowManager(mContext, taskConfig, this); + + mButtonSize = + mContext.getResources().getDimensionPixelSize(R.dimen.size_compat_button_size); + mPopupOffsetX = mButtonSize / 4; + mPopupOffsetY = mButtonSize; + } + + /** Creates the button window. */ + void createSizeCompatButton(boolean isImeShowing) { + if (isImeShowing || mButton != null) { + // When ime is showing, wait until ime is dismiss to create UI. + return; + } + mButton = mWindowManager.createSizeCompatUI(); + updateSurfacePosition(); + } + + /** Releases the button window. */ + void release() { + mButton.remove(); + mButton = null; + mWindowManager.release(); + } + + /** Called when size compat info changed. */ + void updateSizeCompatInfo(Configuration taskConfig, IBinder activityToken, + ShellTaskOrganizer.TaskListener taskListener, boolean isImeShowing) { + final Configuration prevTaskConfig = mTaskConfig; + final ShellTaskOrganizer.TaskListener prevTaskListener = mTaskListener; + mTaskConfig = taskConfig; + mActivityToken = activityToken; + mTaskListener = taskListener; + + // Update configuration. + mContext = mContext.createConfigurationContext(taskConfig); + mWindowManager.setConfiguration(taskConfig); + + if (mButton == null || prevTaskListener != taskListener) { + // TaskListener changed, recreate the button for new surface parent. + release(); + createSizeCompatButton(isImeShowing); + return; + } + + if (!taskConfig.windowConfiguration.getBounds() + .equals(prevTaskConfig.windowConfiguration.getBounds())) { + // Reposition the button surface. + updateSurfacePosition(); + } + + if (taskConfig.getLayoutDirection() != prevTaskConfig.getLayoutDirection()) { + // Update layout for RTL. + mButton.setLayoutDirection(taskConfig.getLayoutDirection()); + updateSurfacePosition(); + } + } + + /** Called when display layout changed. */ + void updateDisplayLayout(DisplayLayout displayLayout) { + if (displayLayout == mDisplayLayout) { + return; + } + + final Rect prevStableBounds = new Rect(); + final Rect curStableBounds = new Rect(); + mDisplayLayout.getStableBounds(prevStableBounds); + displayLayout.getStableBounds(curStableBounds); + mDisplayLayout = displayLayout; + if (!prevStableBounds.equals(curStableBounds)) { + // Stable bounds changed, update button surface position. + updateSurfacePosition(); + } + } + + /** Called when IME visibility changed. */ + void updateImeVisibility(boolean isImeShowing) { + if (mButton == null) { + // Button may not be created because ime is previous showing. + createSizeCompatButton(isImeShowing); + return; + } + + final int newVisibility = isImeShowing ? View.GONE : View.VISIBLE; + if (mButton.getVisibility() != newVisibility) { + mButton.setVisibility(newVisibility); + } + } + + /** Gets the layout params for restart button. */ + WindowManager.LayoutParams getWindowLayoutParams() { + final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams( + mButtonSize, mButtonSize, + TYPE_APPLICATION_OVERLAY, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL, + PixelFormat.TRANSLUCENT); + winParams.gravity = getGravity(getLayoutDirection()); + winParams.token = new Binder(); + winParams.setTitle(SizeCompatRestartButton.class.getSimpleName() + mContext.getDisplayId()); + winParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; + return winParams; + } + + /** Called when it is ready to be placed button surface button. */ + void attachToParentSurface(SurfaceControl.Builder b) { + mTaskListener.attachChildSurfaceToTask(mTaskId, b); + } + + /** Called when the restart button is clicked. */ + void onRestartButtonClicked() { + ActivityClient.getInstance().restartActivityProcessIfVisible(mActivityToken); + } + + @VisibleForTesting + void updateSurfacePosition() { + if (mButton == null || mWindowManager.getSurfaceControl() == null) { + return; + } + // The hint popup won't be at the correct position. + mButton.dismissHint(); + + // Use stable bounds to prevent the button from overlapping with system bars. + final Rect taskBounds = mTaskConfig.windowConfiguration.getBounds(); + final Rect stableBounds = new Rect(); + mDisplayLayout.getStableBounds(stableBounds); + stableBounds.intersect(taskBounds); + + // Position of the button in the container coordinate. + final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL + ? stableBounds.left - taskBounds.left + : stableBounds.right - taskBounds.left - mButtonSize; + final int positionY = stableBounds.bottom - taskBounds.top - mButtonSize; + + mSyncQueue.runInSync(t -> + t.setPosition(mWindowManager.getSurfaceControl(), positionX, positionY)); + } + + int getDisplayId() { + return mDisplayId; + } + + int getTaskId() { + return mTaskId; + } + + private int getLayoutDirection() { + return mContext.getResources().getConfiguration().getLayoutDirection(); + } + + static int getGravity(int layoutDirection) { + return Gravity.BOTTOM + | (layoutDirection == View.LAYOUT_DIRECTION_RTL ? Gravity.START : Gravity.END); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIWindowManager.java new file mode 100644 index 000000000000..a7ad982a4736 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIWindowManager.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2021 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.wm.shell.sizecompatui; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Configuration; +import android.view.IWindow; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.SurfaceSession; +import android.view.WindowlessWindowManager; + +import com.android.wm.shell.R; + +/** + * Holds view hierarchy of a root surface and helps to inflate {@link SizeCompatRestartButton}. + */ +class SizeCompatUIWindowManager extends WindowlessWindowManager { + + private Context mContext; + private final SizeCompatUILayout mLayout; + + @Nullable + private SurfaceControlViewHost mViewHost; + @Nullable + private SurfaceControl mLeash; + + SizeCompatUIWindowManager(Context context, Configuration config, SizeCompatUILayout layout) { + super(config, null /* rootSurface */, null /* hostInputToken */); + mContext = context; + mLayout = layout; + } + + @Override + public void setConfiguration(Configuration configuration) { + super.setConfiguration(configuration); + mContext = mContext.createConfigurationContext(configuration); + } + + @Override + protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later. + final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + .setContainerLayer() + .setName("SizeCompatUILeash") + .setHidden(false) + .setCallsite("SizeCompatUIWindowManager#attachToParentSurface"); + mLayout.attachToParentSurface(builder); + mLeash = builder.build(); + b.setParent(mLeash); + } + + /** Inflates {@link SizeCompatRestartButton} on to the root surface. */ + SizeCompatRestartButton createSizeCompatUI() { + if (mViewHost == null) { + mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + } + + final SizeCompatRestartButton button = (SizeCompatRestartButton) + LayoutInflater.from(mContext).inflate(R.layout.size_compat_ui, null); + button.inject(mLayout); + mViewHost.setView(button, mLayout.getWindowLayoutParams()); + return button; + } + + /** Releases the surface control and tears down the view hierarchy. */ + void release() { + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + + if (mLeash != null) { + new SurfaceControl.Transaction().remove(mLeash).apply(); + mLeash = null; + } + } + + /** + * Gets {@link SurfaceControl} of the surface holding size compat UI view. @return {@code null} + * if not feasible. + */ + @Nullable + SurfaceControl getSurfaceControl() { + return mLeash; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java index 7c1b9d813851..7ca569349633 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -18,59 +18,106 @@ package com.android.wm.shell.splitscreen; import android.annotation.IntDef; import android.app.ActivityManager; +import android.app.PendingIntent; import android.graphics.Rect; import android.os.Bundle; +import android.os.UserHandle; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.draganddrop.DragAndDropPolicy; import java.io.PrintWriter; /** * Interface to engage split-screen feature. + * TODO: Figure out which of these are actually needed outside of the Shell */ @ExternalThread -public interface SplitScreen { +public interface SplitScreen extends DragAndDropPolicy.Starter { /** - * Specifies that the side-stage is positioned at the top half of the screen if + * Stage position isn't specified normally meaning to use what ever it is currently set to. + */ + int STAGE_POSITION_UNDEFINED = -1; + /** + * Specifies that a stage is positioned at the top half of the screen if * in portrait mode or at the left half of the screen if in landscape mode. */ - int SIDE_STAGE_POSITION_TOP_OR_LEFT = 0; + int STAGE_POSITION_TOP_OR_LEFT = 0; /** - * Specifies that the side-stage is positioned at the bottom half of the screen if + * Specifies that a stage is positioned at the bottom half of the screen if * in portrait mode or at the right half of the screen if in landscape mode. */ - int SIDE_STAGE_POSITION_BOTTOM_OR_RIGHT = 1; + int STAGE_POSITION_BOTTOM_OR_RIGHT = 1; + + @IntDef(prefix = { "STAGE_POSITION_" }, value = { + STAGE_POSITION_UNDEFINED, + STAGE_POSITION_TOP_OR_LEFT, + STAGE_POSITION_BOTTOM_OR_RIGHT + }) + @interface StagePosition {} + + /** + * Stage type isn't specified normally meaning to use what ever the default is. + * E.g. exit split-screen and launch the app in fullscreen. + */ + int STAGE_TYPE_UNDEFINED = -1; + /** + * The main stage type. + * @see MainStage + */ + int STAGE_TYPE_MAIN = 0; + + /** + * The side stage type. + * @see SideStage + */ + int STAGE_TYPE_SIDE = 1; - @IntDef(prefix = { "SIDE_STAGE_POSITION_" }, value = { - SIDE_STAGE_POSITION_TOP_OR_LEFT, - SIDE_STAGE_POSITION_BOTTOM_OR_RIGHT + @IntDef(prefix = { "STAGE_TYPE_" }, value = { + STAGE_TYPE_UNDEFINED, + STAGE_TYPE_MAIN, + STAGE_TYPE_SIDE }) - @interface SideStagePosition {} + @interface StageType {} + + /** Callback interface for listening to changes in a split-screen stage. */ + interface SplitScreenListener { + void onStagePositionChanged(@StageType int stage, @StagePosition int position); + void onTaskStageChanged(int taskId, @StageType int stage); + } /** @return {@code true} if split-screen is currently visible. */ boolean isSplitScreenVisible(); /** Moves a task in the side-stage of split-screen. */ - boolean moveToSideStage(int taskId, @SideStagePosition int sideStagePosition); + boolean moveToSideStage(int taskId, @StagePosition int sideStagePosition); /** Moves a task in the side-stage of split-screen. */ boolean moveToSideStage(ActivityManager.RunningTaskInfo task, - @SideStagePosition int sideStagePosition); + @StagePosition int sideStagePosition); /** Removes a task from the side-stage of split-screen. */ boolean removeFromSideStage(int taskId); /** Sets the position of the side-stage. */ - void setSideStagePosition(@SideStagePosition int sideStagePosition); + void setSideStagePosition(@StagePosition int sideStagePosition); /** Hides the side-stage if it is currently visible. */ void setSideStageVisibility(boolean visible); + /** Removes the split-screen stages. */ void exitSplitScreen(); + /** @param exitSplitScreenOnHide if to exit split-screen if both stages are not visible. */ + void exitSplitScreenOnHide(boolean exitSplitScreenOnHide); /** Gets the stage bounds. */ void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds); - /** Updates the launch activity options for the split position we want to launch it in. */ - void updateActivityOptions(Bundle opts, @SideStagePosition int position); - /** Dumps current status of split-screen. */ - void dump(@NonNull PrintWriter pw, String prefix); - /** Called when the shell organizer has been registered. */ - void onOrganizerRegistered(); + + void registerSplitScreenListener(SplitScreenListener listener); + void unregisterSplitScreenListener(SplitScreenListener listener); + + void startTask(int taskId, + @StageType int stage, @StagePosition int position, @Nullable Bundle options); + void startShortcut(String packageName, String shortcutId, @StageType int stage, + @StagePosition int position, @Nullable Bundle options, UserHandle user); + void startIntent(PendingIntent intent, + @StageType int stage, @StagePosition int position, @Nullable Bundle options); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 27d3b81d41b5..b0167afa2e4e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -18,16 +18,33 @@ package com.android.wm.shell.splitscreen; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; + import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.content.ActivityNotFoundException; import android.content.Context; +import android.content.pm.LauncherApps; import android.graphics.Rect; import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Slog; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.draganddrop.DragAndDropPolicy; import java.io.PrintWriter; @@ -36,25 +53,33 @@ import java.io.PrintWriter; * {@link SplitScreen}. * @see StageCoordinator */ -public class SplitScreenController implements SplitScreen { +public class SplitScreenController implements DragAndDropPolicy.Starter { private static final String TAG = SplitScreenController.class.getSimpleName(); private final ShellTaskOrganizer mTaskOrganizer; private final SyncTransactionQueue mSyncQueue; private final Context mContext; private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; + private final ShellExecutor mMainExecutor; + private final SplitScreenImpl mImpl = new SplitScreenImpl(); + private StageCoordinator mStageCoordinator; public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, Context context, - RootTaskDisplayAreaOrganizer rootTDAOrganizer) { + RootTaskDisplayAreaOrganizer rootTDAOrganizer, + ShellExecutor mainExecutor) { mTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; mContext = context; mRootTDAOrganizer = rootTDAOrganizer; + mMainExecutor = mainExecutor; + } + + public SplitScreen asSplitScreen() { + return mImpl; } - @Override public void onOrganizerRegistered() { if (mStageCoordinator == null) { // TODO: Multi-display @@ -63,13 +88,11 @@ public class SplitScreenController implements SplitScreen { } } - @Override public boolean isSplitScreenVisible() { return mStageCoordinator.isSplitScreenVisible(); } - @Override - public boolean moveToSideStage(int taskId, @SideStagePosition int sideStagePosition) { + public boolean moveToSideStage(int taskId, @SplitScreen.StagePosition int sideStagePosition) { final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId); if (task == null) { throw new IllegalArgumentException("Unknown taskId" + taskId); @@ -77,43 +100,136 @@ public class SplitScreenController implements SplitScreen { return moveToSideStage(task, sideStagePosition); } - @Override public boolean moveToSideStage(ActivityManager.RunningTaskInfo task, - @SideStagePosition int sideStagePosition) { + @SplitScreen.StagePosition int sideStagePosition) { return mStageCoordinator.moveToSideStage(task, sideStagePosition); } - @Override public boolean removeFromSideStage(int taskId) { return mStageCoordinator.removeFromSideStage(taskId); } - @Override - public void setSideStagePosition(@SideStagePosition int sideStagePosition) { + public void setSideStagePosition(@SplitScreen.StagePosition int sideStagePosition) { mStageCoordinator.setSideStagePosition(sideStagePosition); } - @Override public void setSideStageVisibility(boolean visible) { mStageCoordinator.setSideStageVisibility(visible); } - @Override + public void enterSplitScreen(int taskId, boolean leftOrTop) { + moveToSideStage(taskId, + leftOrTop ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT); + } + public void exitSplitScreen() { mStageCoordinator.exitSplitScreen(); } - @Override + public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { + mStageCoordinator.exitSplitScreenOnHide(exitSplitScreenOnHide); + } + public void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { mStageCoordinator.getStageBounds(outTopOrLeftBounds, outBottomOrRightBounds); } - @Override - public void updateActivityOptions(Bundle opts, @SideStagePosition int position) { - mStageCoordinator.updateActivityOptions(opts, position); + public void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) { + mStageCoordinator.registerSplitScreenListener(listener); + } + + public void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) { + mStageCoordinator.unregisterSplitScreenListener(listener); + } + + public void startTask(int taskId, @SplitScreen.StageType int stage, + @SplitScreen.StagePosition int position, @Nullable Bundle options) { + options = resolveStartStage(stage, position, options); + + try { + ActivityTaskManager.getService().startActivityFromRecents(taskId, options); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to launch task", e); + } + } + + public void startShortcut(String packageName, String shortcutId, + @SplitScreen.StageType int stage, @SplitScreen.StagePosition int position, + @Nullable Bundle options, UserHandle user) { + options = resolveStartStage(stage, position, options); + + try { + LauncherApps launcherApps = + mContext.getSystemService(LauncherApps.class); + launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, + options, user); + } catch (ActivityNotFoundException e) { + Slog.e(TAG, "Failed to launch shortcut", e); + } + } + + public void startIntent(PendingIntent intent, @SplitScreen.StageType int stage, + @SplitScreen.StagePosition int position, @Nullable Bundle options) { + options = resolveStartStage(stage, position, options); + + try { + intent.send(null, 0, null, null, null, null, options); + } catch (PendingIntent.CanceledException e) { + Slog.e(TAG, "Failed to launch activity", e); + } + } + + private Bundle resolveStartStage(@SplitScreen.StageType int stage, + @SplitScreen.StagePosition int position, @Nullable Bundle options) { + switch (stage) { + case STAGE_TYPE_UNDEFINED: { + // Use the stage of the specified position is valid. + if (position != STAGE_POSITION_UNDEFINED) { + if (position == mStageCoordinator.getSideStagePosition()) { + options = resolveStartStage(STAGE_TYPE_SIDE, position, options); + } else { + options = resolveStartStage(STAGE_TYPE_MAIN, position, options); + } + } else { + // Exit split-screen and launch fullscreen since stage wasn't specified. + mStageCoordinator.exitSplitScreen(); + } + break; + } + case STAGE_TYPE_SIDE: { + if (position != STAGE_POSITION_UNDEFINED) { + mStageCoordinator.setSideStagePosition(position); + } else { + position = mStageCoordinator.getSideStagePosition(); + } + if (options == null) { + options = new Bundle(); + } + mStageCoordinator.updateActivityOptions(options, position); + break; + } + case STAGE_TYPE_MAIN: { + if (position != STAGE_POSITION_UNDEFINED) { + // Set the side stage opposite of what we want to the main stage. + final int sideStagePosition = position == STAGE_POSITION_TOP_OR_LEFT + ? STAGE_POSITION_BOTTOM_OR_RIGHT : STAGE_POSITION_TOP_OR_LEFT; + mStageCoordinator.setSideStagePosition(sideStagePosition); + } else { + position = mStageCoordinator.getMainStagePosition(); + } + if (options == null) { + options = new Bundle(); + } + mStageCoordinator.updateActivityOptions(options, position); + break; + } + default: + throw new IllegalArgumentException("Unknown stage=" + stage); + } + + return options; } - @Override public void dump(@NonNull PrintWriter pw, String prefix) { pw.println(prefix + TAG); if (mStageCoordinator != null) { @@ -121,4 +237,120 @@ public class SplitScreenController implements SplitScreen { } } + private class SplitScreenImpl implements SplitScreen { + @Override + public boolean isSplitScreenVisible() { + return mMainExecutor.executeBlockingForResult(() -> { + return SplitScreenController.this.isSplitScreenVisible(); + }, Boolean.class); + } + + @Override + public boolean moveToSideStage(int taskId, int sideStagePosition) { + return mMainExecutor.executeBlockingForResult(() -> { + return SplitScreenController.this.moveToSideStage(taskId, sideStagePosition); + }, Boolean.class); + } + + @Override + public boolean moveToSideStage(ActivityManager.RunningTaskInfo task, + int sideStagePosition) { + return mMainExecutor.executeBlockingForResult(() -> { + return SplitScreenController.this.moveToSideStage(task, sideStagePosition); + }, Boolean.class); + } + + @Override + public boolean removeFromSideStage(int taskId) { + return mMainExecutor.executeBlockingForResult(() -> { + return SplitScreenController.this.removeFromSideStage(taskId); + }, Boolean.class); + } + + @Override + public void setSideStagePosition(int sideStagePosition) { + mMainExecutor.execute(() -> { + SplitScreenController.this.setSideStagePosition(sideStagePosition); + }); + } + + @Override + public void setSideStageVisibility(boolean visible) { + mMainExecutor.execute(() -> { + SplitScreenController.this.setSideStageVisibility(visible); + }); + } + + @Override + public void enterSplitScreen(int taskId, boolean leftOrTop) { + mMainExecutor.execute(() -> { + SplitScreenController.this.enterSplitScreen(taskId, leftOrTop); + }); + } + + @Override + public void exitSplitScreen() { + mMainExecutor.execute(() -> { + SplitScreenController.this.exitSplitScreen(); + }); + } + + @Override + public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { + mMainExecutor.execute(() -> { + SplitScreenController.this.exitSplitScreenOnHide(exitSplitScreenOnHide); + }); + } + + @Override + public void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { + try { + mMainExecutor.executeBlocking(() -> { + SplitScreenController.this.getStageBounds(outTopOrLeftBounds, + outBottomOrRightBounds); + }); + } catch (InterruptedException e) { + Slog.e(TAG, "Failed to get stage bounds in 2s"); + } + } + + @Override + public void registerSplitScreenListener(SplitScreenListener listener) { + mMainExecutor.execute(() -> { + SplitScreenController.this.registerSplitScreenListener(listener); + }); + } + + @Override + public void unregisterSplitScreenListener(SplitScreenListener listener) { + mMainExecutor.execute(() -> { + SplitScreenController.this.unregisterSplitScreenListener(listener); + }); + } + + @Override + public void startTask(int taskId, int stage, int position, @Nullable Bundle options) { + mMainExecutor.execute(() -> { + SplitScreenController.this.startTask(taskId, stage, position, options); + }); + } + + @Override + public void startShortcut(String packageName, String shortcutId, int stage, int position, + @Nullable Bundle options, UserHandle user) { + mMainExecutor.execute(() -> { + SplitScreenController.this.startShortcut(packageName, shortcutId, stage, position, + options, user); + }); + } + + @Override + public void startIntent(PendingIntent intent, int stage, int position, + @Nullable Bundle options) { + mMainExecutor.execute(() -> { + SplitScreenController.this.startIntent(intent, stage, position, options); + }); + } + } + } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index d571e7514542..e44c820a656a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -17,12 +17,14 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; -import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; -import static com.android.wm.shell.splitscreen.SplitScreen.SIDE_STAGE_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.splitscreen.SplitScreen.SIDE_STAGE_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import android.app.ActivityManager; import android.content.Context; @@ -41,6 +43,8 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.split.SplitLayout; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; /** * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and @@ -64,8 +68,7 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener, private final StageListenerImpl mMainStageListener = new StageListenerImpl(); private final SideStage mSideStage; private final StageListenerImpl mSideStageListener = new StageListenerImpl(); - private @SplitScreen.SideStagePosition int mSideStagePosition = - SIDE_STAGE_POSITION_BOTTOM_OR_RIGHT; + private @SplitScreen.StagePosition int mSideStagePosition = STAGE_POSITION_BOTTOM_OR_RIGHT; private final int mDisplayId; private SplitLayout mSplitLayout; @@ -75,6 +78,8 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener, private final ShellTaskOrganizer mTaskOrganizer; private DisplayAreaInfo mDisplayAreaInfo; private final Context mContext; + private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>(); + private boolean mExitSplitScreenOnHide = true; StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer) { @@ -107,9 +112,9 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener, } boolean moveToSideStage(ActivityManager.RunningTaskInfo task, - @SplitScreen.SideStagePosition int sideStagePosition) { + @SplitScreen.StagePosition int sideStagePosition) { final WindowContainerTransaction wct = new WindowContainerTransaction(); - mSideStagePosition = sideStagePosition; + setSideStagePosition(sideStagePosition); mMainStage.activate(getMainStageBounds(), wct); mSideStage.addTask(task, getSideStageBounds(), wct); mTaskOrganizer.applyTransaction(wct); @@ -130,15 +135,28 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener, return result; } - void setSideStagePosition(@SplitScreen.SideStagePosition int sideStagePosition) { + @SplitScreen.StagePosition int getSideStagePosition() { + return mSideStagePosition; + } + + @SplitScreen.StagePosition int getMainStagePosition() { + return mSideStagePosition == STAGE_POSITION_TOP_OR_LEFT + ? STAGE_POSITION_BOTTOM_OR_RIGHT : STAGE_POSITION_TOP_OR_LEFT; + } + + void setSideStagePosition(@SplitScreen.StagePosition int sideStagePosition) { + if (mSideStagePosition == sideStagePosition) return; + mSideStagePosition = sideStagePosition; if (mSideStageListener.mVisible) { onStageVisibilityChanged(mSideStageListener); } + + sendOnStagePositionChanged(); } void setSideStageVisibility(boolean visible) { - if (!mSideStageListener.mVisible == visible) return; + if (mSideStageListener.mVisible == visible) return; final WindowContainerTransaction wct = new WindowContainerTransaction(); mSideStage.setVisibility(visible, wct); @@ -149,6 +167,10 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener, exitSplitScreen(null /* childrenToTop */); } + void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { + mExitSplitScreenOnHide = exitSplitScreenOnHide; + } + private void exitSplitScreen(StageTaskListener childrenToTop) { final WindowContainerTransaction wct = new WindowContainerTransaction(); mSideStage.removeAllTasks(wct, childrenToTop == mSideStage); @@ -163,7 +185,7 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener, outBottomOrRightBounds.set(mSplitLayout.getBounds2()); } - void updateActivityOptions(Bundle opts, @SplitScreen.SideStagePosition int position) { + void updateActivityOptions(Bundle opts, @SplitScreen.StagePosition int position) { final StageTaskListener stage = position == mSideStagePosition ? mSideStage : mMainStage; opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token); @@ -176,6 +198,43 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener, } } + void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) { + if (mListeners.contains(listener)) return; + mListeners.add(listener); + listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); + listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); + mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE); + mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN); + } + + void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) { + mListeners.remove(listener); + } + + private void sendOnStagePositionChanged() { + for (int i = mListeners.size() - 1; i >= 0; --i) { + final SplitScreen.SplitScreenListener l = mListeners.get(i); + l.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); + l.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); + } + } + + private void onStageChildTaskStatusChanged( + StageListenerImpl stageListener, int taskId, boolean present) { + + int stage; + if (present) { + stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; + } else { + // No longer on any stage + stage = STAGE_TYPE_UNDEFINED; + } + + for (int i = mListeners.size() - 1; i >= 0; --i) { + mListeners.get(i).onTaskStageChanged(taskId, stage); + } + } + private void onStageRootTaskAppeared(StageListenerImpl stageListener) { if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) { final WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -209,7 +268,7 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener, } } - if (!mainStageVisible && !sideStageVisible) { + if (mExitSplitScreenOnHide && !mainStageVisible && !sideStageVisible) { // Exit split-screen if both stage are not visible. // TODO: This is only a temporary request from UX and is likely to be removed soon... exitSplitScreen(); @@ -299,7 +358,7 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener, @Override public void onSnappedToDismiss(boolean bottomOrRight) { final boolean mainStageToTop = bottomOrRight - && mSideStagePosition == SIDE_STAGE_POSITION_BOTTOM_OR_RIGHT; + && mSideStagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT; exitSplitScreen(mainStageToTop ? mMainStage : mSideStage); } @@ -326,8 +385,8 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener, @Override public void onDoubleTappedDivider() { - setSideStagePosition(mSideStagePosition == SIDE_STAGE_POSITION_TOP_OR_LEFT - ? SIDE_STAGE_POSITION_BOTTOM_OR_RIGHT : SIDE_STAGE_POSITION_TOP_OR_LEFT); + setSideStagePosition(mSideStagePosition == STAGE_POSITION_TOP_OR_LEFT + ? STAGE_POSITION_BOTTOM_OR_RIGHT : STAGE_POSITION_TOP_OR_LEFT); } @Override @@ -380,12 +439,12 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener, } private Rect getSideStageBounds() { - return mSideStagePosition == SIDE_STAGE_POSITION_TOP_OR_LEFT + return mSideStagePosition == STAGE_POSITION_TOP_OR_LEFT ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2(); } private Rect getMainStageBounds() { - return mSideStagePosition == SIDE_STAGE_POSITION_TOP_OR_LEFT + return mSideStagePosition == STAGE_POSITION_TOP_OR_LEFT ? mSplitLayout.getBounds2() : mSplitLayout.getBounds1(); } @@ -429,6 +488,11 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener, } @Override + public void onChildTaskStatusChanged(int taskId, boolean present) { + StageCoordinator.this.onStageChildTaskStatusChanged(this, taskId, present); + } + + @Override public void onRootTaskVanished() { reset(); StageCoordinator.this.onStageRootTaskVanished(this); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index 1aa7552c01eb..10c742b69578 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -58,6 +58,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { public interface StageListenerCallbacks { void onRootTaskAppeared(); void onStatusChanged(boolean visible, boolean hasChildren); + void onChildTaskStatusChanged(int taskId, boolean present); void onRootTaskVanished(); } private final StageListenerCallbacks mCallbacks; @@ -83,9 +84,11 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { mRootTaskInfo = taskInfo; mCallbacks.onRootTaskAppeared(); } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { - mChildrenLeashes.put(taskInfo.taskId, leash); - mChildrenTaskInfo.put(taskInfo.taskId, taskInfo); + final int taskId = taskInfo.taskId; + mChildrenLeashes.put(taskId, leash); + mChildrenTaskInfo.put(taskId, taskInfo); updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */); + mCallbacks.onChildTaskStatusChanged(taskId, true /* present */); } else { throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + "\n mRootTaskInfo: " + mRootTaskInfo); @@ -120,12 +123,24 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { mChildrenTaskInfo.remove(taskId); mChildrenLeashes.remove(taskId); sendStatusChanged(); + mCallbacks.onChildTaskStatusChanged(taskId, false /* present */); } else { throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + "\n mRootTaskInfo: " + mRootTaskInfo); } } + @Override + public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { + if (mRootTaskInfo.taskId == taskId) { + b.setParent(mRootLeash); + } else if (mChildrenLeashes.contains(taskId)) { + b.setParent(mChildrenLeashes.get(taskId)); + } else { + throw new IllegalArgumentException("There is no surface for taskId=" + taskId); + } + } + void setBounds(Rect bounds, WindowContainerTransaction wct) { wct.setBounds(mRootTaskInfo.token, bounds); } @@ -134,6 +149,13 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { wct.reorder(mRootTaskInfo.token, visible /* onTop */); } + void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener, + @SplitScreen.StageType int stage) { + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { + listener.onTaskStageChanged(mChildrenTaskInfo.keyAt(i), stage); + } + } + private void updateChildTaskSurface(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, boolean firstAppeared) { final Point taskPositionInParent = taskInfo.positionInParent; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index f3f2fc3686b6..45d551528940 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -31,23 +31,21 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.os.Build; import android.util.Slog; -import android.view.Gravity; -import android.view.View; -import android.view.WindowManager; -import android.widget.FrameLayout; +import android.view.Window; +import android.window.SplashScreenView; import com.android.internal.R; import com.android.internal.graphics.palette.Palette; import com.android.internal.graphics.palette.Quantizer; import com.android.internal.graphics.palette.VariationalKMeansQuantizer; -import com.android.internal.policy.PhoneWindow; import java.util.List; /** * Util class to create the view for a splash screen content. + * @hide */ -class SplashscreenContentDrawer { +public class SplashscreenContentDrawer { private static final String TAG = StartingSurfaceDrawer.TAG; private static final boolean DEBUG = StartingSurfaceDrawer.DEBUG_SPLASH_SCREEN; @@ -58,15 +56,24 @@ class SplashscreenContentDrawer { // also 108*108 pixels, then do not enlarge this icon if only need to show foreground icon. private static final float ENLARGE_FOREGROUND_ICON_THRESHOLD = (72f * 72f) / (108f * 108f); private final Context mContext; + private final int mMaxIconAnimationDuration; + private int mIconSize; + private int mBrandingImageWidth; + private int mBrandingImageHeight; - SplashscreenContentDrawer(Context context) { + SplashscreenContentDrawer(Context context, int maxIconAnimationDuration) { mContext = context; + mMaxIconAnimationDuration = maxIconAnimationDuration; } private void updateDensity() { mIconSize = mContext.getResources().getDimensionPixelSize( com.android.wm.shell.R.dimen.starting_surface_icon_size); + mBrandingImageWidth = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.starting_surface_brand_image_width); + mBrandingImageHeight = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.starting_surface_brand_image_height); } private int getSystemBGColor() { @@ -83,48 +90,92 @@ class SplashscreenContentDrawer { return new ColorDrawable(getSystemBGColor()); } - View makeSplashScreenContentView(PhoneWindow win, Context context, int iconRes, + SplashScreenView makeSplashScreenContentView(Window win, Context context, int iconRes, int splashscreenContentResId) { updateDensity(); - win.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); // splash screen content will be deprecated after S. - final View ssc = makeSplashscreenContentDrawable(win, context, splashscreenContentResId); + final SplashScreenView ssc = + makeSplashscreenContentDrawable(win, context, splashscreenContentResId); if (ssc != null) { return ssc; } - final TypedArray typedArray = context.obtainStyledAttributes( - com.android.internal.R.styleable.Window); - final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0); - typedArray.recycle(); + final SplashScreenWindowAttrs attrs = + SplashScreenWindowAttrs.createWindowAttrs(context); + final StartingWindowViewBuilder builder = new StartingWindowViewBuilder(); final Drawable themeBGDrawable; - if (resId == 0) { + + if (attrs.mWindowBgColor != 0) { + themeBGDrawable = new ColorDrawable(attrs.mWindowBgColor); + } else if (attrs.mWindowBgResId != 0) { + themeBGDrawable = context.getDrawable(attrs.mWindowBgResId); + } else { Slog.w(TAG, "Window background not exist!"); themeBGDrawable = createDefaultBackgroundDrawable(); + } + final int animationDuration; + final Drawable iconDrawable; + if (attrs.mReplaceIcon != null) { + iconDrawable = attrs.mReplaceIcon; + animationDuration = Math.max(0, + Math.min(attrs.mAnimationDuration, mMaxIconAnimationDuration)); } else { - themeBGDrawable = context.getDrawable(resId); + iconDrawable = iconRes != 0 ? context.getDrawable(iconRes) + : context.getPackageManager().getDefaultActivityIcon(); + animationDuration = 0; } - final Drawable iconDrawable = iconRes != 0 ? context.getDrawable(iconRes) - : context.getPackageManager().getDefaultActivityIcon(); // TODO (b/173975965) Tracking the performance on improved splash screen. - final StartingWindowViewBuilder builder = new StartingWindowViewBuilder(); return builder - .setPhoneWindow(win) + .setWindow(win) .setContext(context) .setThemeDrawable(themeBGDrawable) - .setIconDrawable(iconDrawable).build(); + .setIconDrawable(iconDrawable) + .setIconAnimationDuration(animationDuration) + .setBrandingDrawable(attrs.mBrandingImage).build(); + } + + private static class SplashScreenWindowAttrs { + private int mWindowBgResId = 0; + private int mWindowBgColor = Color.TRANSPARENT; + private Drawable mReplaceIcon = null; + private Drawable mBrandingImage = null; + private int mAnimationDuration = 0; + + static SplashScreenWindowAttrs createWindowAttrs(Context context) { + final SplashScreenWindowAttrs attrs = new SplashScreenWindowAttrs(); + final TypedArray typedArray = context.obtainStyledAttributes( + com.android.internal.R.styleable.Window); + attrs.mWindowBgResId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0); + attrs.mWindowBgColor = typedArray.getColor( + R.styleable.Window_windowSplashScreenBackground, Color.TRANSPARENT); + attrs.mReplaceIcon = typedArray.getDrawable( + R.styleable.Window_windowSplashScreenAnimatedIcon); + attrs.mAnimationDuration = typedArray.getInt( + R.styleable.Window_windowSplashScreenAnimationDuration, 0); + attrs.mBrandingImage = typedArray.getDrawable( + R.styleable.Window_windowSplashScreenBrandingImage); + typedArray.recycle(); + if (DEBUG) { + Slog.d(TAG, "window attributes color: " + + Integer.toHexString(attrs.mWindowBgColor) + + " icon " + attrs.mReplaceIcon + " duration " + attrs.mAnimationDuration + + " brandImage " + attrs.mBrandingImage); + } + return attrs; + } } private class StartingWindowViewBuilder { - // materials private Drawable mThemeBGDrawable; private Drawable mIconDrawable; - private PhoneWindow mPhoneWindow; + private Window mWindow; + private int mIconAnimationDuration; private Context mContext; + private Drawable mBrandingDrawable; // result private boolean mBuildComplete = false; - private View mCachedResult; + private SplashScreenView mCachedResult; private int mThemeColor; private Drawable mFinalIconDrawable; private float mScale = 1f; @@ -141,8 +192,20 @@ class SplashscreenContentDrawer { return this; } - StartingWindowViewBuilder setPhoneWindow(PhoneWindow window) { - mPhoneWindow = window; + StartingWindowViewBuilder setWindow(Window window) { + mWindow = window; + mBuildComplete = false; + return this; + } + + StartingWindowViewBuilder setIconAnimationDuration(int iconAnimationDuration) { + mIconAnimationDuration = iconAnimationDuration; + mBuildComplete = false; + return this; + } + + StartingWindowViewBuilder setBrandingDrawable(Drawable branding) { + mBrandingDrawable = branding; mBuildComplete = false; return this; } @@ -153,11 +216,11 @@ class SplashscreenContentDrawer { return this; } - View build() { + SplashScreenView build() { if (mBuildComplete) { return mCachedResult; } - if (mPhoneWindow == null || mContext == null) { + if (mWindow == null || mContext == null) { Slog.e(TAG, "Unable to create StartingWindowView, lack of materials!"); return null; } @@ -173,7 +236,7 @@ class SplashscreenContentDrawer { mFinalIconDrawable = mIconDrawable; } final int iconSize = mFinalIconDrawable != null ? (int) (mIconSize * mScale) : 0; - mCachedResult = fillViewWithIcon(mPhoneWindow, mContext, iconSize, mFinalIconDrawable); + mCachedResult = fillViewWithIcon(mWindow, mContext, iconSize, mFinalIconDrawable); mBuildComplete = true; return mCachedResult; } @@ -249,25 +312,26 @@ class SplashscreenContentDrawer { return true; } - private View fillViewWithIcon(PhoneWindow win, Context context, + private SplashScreenView fillViewWithIcon(Window win, Context context, int iconSize, Drawable iconDrawable) { - final StartingSurfaceWindowView surfaceWindowView = - new StartingSurfaceWindowView(context, iconSize); - surfaceWindowView.setBackground(new ColorDrawable(mThemeColor)); + final SplashScreenView.Builder builder = new SplashScreenView.Builder(context); + builder.setIconSize(iconSize).setBackgroundColor(mThemeColor); if (iconDrawable != null) { - surfaceWindowView.setIconDrawable(iconDrawable); + builder.setCenterViewDrawable(iconDrawable); } + builder.setAnimationDuration(mIconAnimationDuration); + if (mBrandingDrawable != null) { + builder.setBrandingDrawable(mBrandingDrawable, mBrandingImageWidth, + mBrandingImageHeight); + } + final SplashScreenView splashScreenView = builder.build(); if (DEBUG) { - Slog.d(TAG, "fillViewWithIcon surfaceWindowView " + surfaceWindowView); + Slog.d(TAG, "fillViewWithIcon surfaceWindowView " + splashScreenView); } - win.setContentView(surfaceWindowView); - makeSystemUIColorsTransparent(win); - return surfaceWindowView; - } - - private void makeSystemUIColorsTransparent(PhoneWindow win) { - win.setStatusBarColor(Color.TRANSPARENT); - win.setNavigationBarColor(Color.TRANSPARENT); + win.setContentView(splashScreenView); + splashScreenView.cacheRootWindow(win); + splashScreenView.makeSystemUIColorsTransparent(); + return splashScreenView; } } @@ -298,8 +362,8 @@ class SplashscreenContentDrawer { return root < 0.1; } - private static View makeSplashscreenContentDrawable(PhoneWindow win, Context ctx, - int splashscreenContentResId) { + private static SplashScreenView makeSplashscreenContentDrawable(Window win, + Context ctx, int splashscreenContentResId) { // doesn't support windowSplashscreenContent after S // TODO add an allowlist to skip some packages if needed final int targetSdkVersion = ctx.getApplicationInfo().targetSdkVersion; @@ -316,7 +380,8 @@ class SplashscreenContentDrawer { if (drawable == null) { return null; } - View view = new View(ctx); + SplashScreenView view = new SplashScreenView(ctx); + view.setNotCopyable(); view.setBackground(drawable); win.setContentView(view); return view; @@ -532,34 +597,4 @@ class SplashscreenContentDrawer { } } } - - private static class StartingSurfaceWindowView extends FrameLayout { - // TODO animate the icon view - private final View mIconView; - - StartingSurfaceWindowView(Context context, int iconSize) { - super(context); - - final boolean emptyIcon = iconSize == 0; - if (emptyIcon) { - mIconView = null; - } else { - mIconView = new View(context); - FrameLayout.LayoutParams params = - new FrameLayout.LayoutParams(iconSize, iconSize); - params.gravity = Gravity.CENTER; - addView(mIconView, params); - } - setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); - } - - // TODO support animatable icon - void setIconDrawable(Drawable icon) { - if (mIconView != null) { - mIconView.setBackground(icon); - } - } - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java index 8e24e0b516cb..5332291fd1bd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java @@ -43,6 +43,8 @@ import android.util.SparseArray; import android.view.Display; import android.view.View; import android.view.WindowManager; +import android.window.SplashScreenView; +import android.window.SplashScreenView.SplashScreenViewParcelable; import android.window.StartingWindowInfo; import android.window.TaskOrganizer; import android.window.TaskSnapshot; @@ -63,7 +65,6 @@ import java.util.function.Consumer; * class to remove the starting window of the Task. * @hide */ - public class StartingSurfaceDrawer { static final String TAG = StartingSurfaceDrawer.class.getSimpleName(); static final boolean DEBUG_SPLASH_SCREEN = false; @@ -81,7 +82,10 @@ public class StartingSurfaceDrawer { mContext = context; mDisplayManager = mContext.getSystemService(DisplayManager.class); mMainExecutor = mainExecutor; - mSplashscreenContentDrawer = new SplashscreenContentDrawer(context); + + final int maxIconAnimDuration = context.getResources().getInteger( + com.android.wm.shell.R.integer.max_starting_window_intro_icon_anim_duration); + mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, maxIconAnimDuration); } private final SparseArray<StartingWindowRecord> mStartingWindowRecords = new SparseArray<>(); @@ -193,9 +197,8 @@ public class StartingSurfaceDrawer { public void addStartingWindow(StartingWindowInfo windowInfo, IBinder appToken) { final PreferredStartingTypeHelper helper = new PreferredStartingTypeHelper(windowInfo); - final RunningTaskInfo runningTaskInfo = windowInfo.taskInfo; if (helper.mPreferredType == PreferredStartingTypeHelper.STARTING_TYPE_SPLASH_SCREEN) { - addSplashScreenStartingWindow(runningTaskInfo, appToken); + addSplashScreenStartingWindow(windowInfo, appToken); } else if (helper.mPreferredType == PreferredStartingTypeHelper.STARTING_TYPE_SNAPSHOT) { final TaskSnapshot snapshot = helper.mSnapshot; makeTaskSnapshotWindow(windowInfo, appToken, snapshot); @@ -203,11 +206,13 @@ public class StartingSurfaceDrawer { // If prefer don't show, then don't show! } - private void addSplashScreenStartingWindow(RunningTaskInfo taskInfo, IBinder appToken) { + private void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken) { + final RunningTaskInfo taskInfo = windowInfo.taskInfo; final ActivityInfo activityInfo = taskInfo.topActivityInfo; if (activityInfo == null) { return; } + final int displayId = taskInfo.displayId; if (activityInfo.packageName == null) { return; @@ -222,11 +227,11 @@ public class StartingSurfaceDrawer { } Context context = mContext; - int theme = activityInfo.getThemeResource(); - if (theme == 0) { - // replace with the default theme if the application didn't set - theme = com.android.internal.R.style.Theme_DeviceDefault_DayNight; - } + // replace with the default theme if the application didn't set + final int theme = windowInfo.splashScreenThemeResId != 0 + ? windowInfo.splashScreenThemeResId + : activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource() + : com.android.internal.R.style.Theme_DeviceDefault_DayNight; if (DEBUG_SPLASH_SCREEN) { Slog.d(TAG, "addSplashScreen " + activityInfo.packageName + ": nonLocalizedLabel=" + nonLocalizedLabel + " theme=" @@ -352,9 +357,10 @@ public class StartingSurfaceDrawer { } params.setTitle("Splash Screen " + activityInfo.packageName); - final View contentView = mSplashscreenContentDrawer.makeSplashScreenContentView(win, - context, iconRes, splashscreenContentResId[0]); - if (contentView == null) { + final SplashScreenView splashScreenView = + mSplashscreenContentDrawer.makeSplashScreenContentView(win, context, iconRes, + splashscreenContentResId[0]); + if (splashScreenView == null) { Slog.w(TAG, "Adding splash screen window for " + activityInfo.packageName + " failed!"); return; } @@ -366,7 +372,7 @@ public class StartingSurfaceDrawer { + activityInfo.packageName + " / " + appToken + ": " + view); } final WindowManager wm = context.getSystemService(WindowManager.class); - postAddWindow(taskInfo.taskId, appToken, view, wm, params); + postAddWindow(taskInfo.taskId, appToken, view, wm, params, splashScreenView); } /** @@ -377,12 +383,10 @@ public class StartingSurfaceDrawer { final int taskId = startingWindowInfo.taskInfo.taskId; final TaskSnapshotWindow surface = TaskSnapshotWindow.create(startingWindowInfo, appToken, snapshot, mMainExecutor, () -> removeWindowSynced(taskId) /* clearWindow */); - mMainExecutor.execute(() -> { - mMainExecutor.executeDelayed(() -> removeWindowSynced(taskId), REMOVE_WHEN_TIMEOUT); - final StartingWindowRecord tView = - new StartingWindowRecord(null/* decorView */, surface); - mStartingWindowRecords.put(taskId, tView); - }); + mMainExecutor.executeDelayed(() -> removeWindowSynced(taskId), REMOVE_WHEN_TIMEOUT); + final StartingWindowRecord tView = + new StartingWindowRecord(null/* decorView */, surface, null /* splashScreenView */); + mStartingWindowRecords.put(taskId, tView); } /** @@ -392,11 +396,32 @@ public class StartingSurfaceDrawer { if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { Slog.d(TAG, "Task start finish, remove starting surface for task " + taskId); } - mMainExecutor.execute(() -> removeWindowSynced(taskId)); + removeWindowSynced(taskId); + } + + /** + * Called when the Task wants to copy the splash screen. + * @param taskId + */ + public void copySplashScreenView(int taskId) { + final StartingWindowRecord preView = mStartingWindowRecords.get(taskId); + SplashScreenViewParcelable parcelable; + if (preView != null && preView.mContentView != null + && preView.mContentView.isCopyable()) { + parcelable = new SplashScreenViewParcelable(preView.mContentView); + } else { + parcelable = null; + } + if (DEBUG_SPLASH_SCREEN) { + Slog.v(TAG, "Copying splash screen window view for task: " + taskId + + " parcelable? " + parcelable); + } + ActivityTaskManager.getInstance().onSplashScreenViewCopyFinished(taskId, parcelable); } protected void postAddWindow(int taskId, IBinder appToken, - View view, WindowManager wm, WindowManager.LayoutParams params) { + View view, WindowManager wm, WindowManager.LayoutParams params, + SplashScreenView splashScreenView) { mMainExecutor.execute(() -> { boolean shouldSaveView = true; try { @@ -419,12 +444,12 @@ public class StartingSurfaceDrawer { shouldSaveView = false; } } - if (shouldSaveView) { removeWindowSynced(taskId); mMainExecutor.executeDelayed(() -> removeWindowSynced(taskId), REMOVE_WHEN_TIMEOUT); - final StartingWindowRecord tView = - new StartingWindowRecord(view, null /* TaskSnapshotWindow */); + final StartingWindowRecord tView = new StartingWindowRecord(view, + null /* TaskSnapshotWindow */, splashScreenView); + splashScreenView.startIntroAnimation(); mStartingWindowRecords.put(taskId, tView); } }); @@ -445,7 +470,7 @@ public class StartingSurfaceDrawer { if (DEBUG_TASK_SNAPSHOT) { Slog.v(TAG, "Removing task snapshot window for " + taskId); } - record.mTaskSnapshotWindow.remove(mMainExecutor); + record.mTaskSnapshotWindow.remove(); } mStartingWindowRecords.remove(taskId); } @@ -463,10 +488,13 @@ public class StartingSurfaceDrawer { private static class StartingWindowRecord { private final View mDecorView; private final TaskSnapshotWindow mTaskSnapshotWindow; + private final SplashScreenView mContentView; - StartingWindowRecord(View decorView, TaskSnapshotWindow taskSnapshotWindow) { + StartingWindowRecord(View decorView, TaskSnapshotWindow taskSnapshotWindow, + SplashScreenView splashScreenView) { mDecorView = decorView; mTaskSnapshotWindow = taskSnapshotWindow; + mContentView = splashScreenView; } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java index 319872556365..b7fd3cb67b2b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -43,6 +43,7 @@ import static com.android.internal.policy.DecorView.NAVIGATION_BAR_COLOR_VIEW_AT import static com.android.internal.policy.DecorView.STATUS_BAR_COLOR_VIEW_ATTRIBUTES; import static com.android.internal.policy.DecorView.getNavigationBarRect; +import android.annotation.BinderThread; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManager.TaskDescription; @@ -82,6 +83,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.DecorView; import com.android.internal.view.BaseIWindow; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ExternalThread; /** * This class represents a starting window that shows a snapshot. @@ -121,6 +123,7 @@ public class TaskSnapshotWindow { private final Window mWindow; private final Surface mSurface; private final Runnable mClearWindowHandler; + private final ShellExecutor mMainExecutor; private SurfaceControl mSurfaceControl; private SurfaceControl mChildSurfaceControl; private final IWindowSession mSession; @@ -213,16 +216,15 @@ public class TaskSnapshotWindow { final TaskSnapshotWindow snapshotSurface = new TaskSnapshotWindow( surfaceControl, snapshot, layoutParams.getTitle(), taskDescription, appearance, windowFlags, windowPrivateFlags, taskBounds, orientation, activityType, - topWindowInsetsState, clearWindowHandler); + topWindowInsetsState, clearWindowHandler, mainExecutor); final Window window = snapshotSurface.mWindow; final InsetsState mTmpInsetsState = new InsetsState(); final InputChannel tmpInputChannel = new InputChannel(); mainExecutor.execute(() -> { try { - final int res = session.addToDisplay(window, layoutParams, View.GONE, - displayId, mTmpInsetsState, tmpFrames.frame, tmpInputChannel, - mTmpInsetsState, mTempControls); + final int res = session.addToDisplay(window, layoutParams, View.GONE, displayId, + mTmpInsetsState, tmpInputChannel, mTmpInsetsState, mTempControls); if (res < 0) { Slog.w(TAG, "Failed to add snapshot starting window res=" + res); return; @@ -230,7 +232,7 @@ public class TaskSnapshotWindow { } catch (RemoteException e) { snapshotSurface.clearWindowSynced(); } - window.setOuter(snapshotSurface, mainExecutor); + window.setOuter(snapshotSurface); try { session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, -1, tmpFrames, tmpMergedConfiguration, surfaceControl, mTmpInsetsState, @@ -250,7 +252,8 @@ public class TaskSnapshotWindow { TaskSnapshot snapshot, CharSequence title, TaskDescription taskDescription, int appearance, int windowFlags, int windowPrivateFlags, Rect taskBounds, int currentOrientation, int activityType, InsetsState topWindowInsetsState, - Runnable clearWindowHandler) { + Runnable clearWindowHandler, ShellExecutor mainExecutor) { + mMainExecutor = mainExecutor; mSurface = new Surface(); mSession = WindowManagerGlobal.getWindowSession(); mWindow = new Window(); @@ -287,28 +290,26 @@ public class TaskSnapshotWindow { mSystemBarBackgroundPainter.drawNavigationBarBackground(c); } - void remove(ShellExecutor mainExecutor) { + void remove() { final long now = SystemClock.uptimeMillis(); if (mSizeMismatch && now - mShownTime < SIZE_MISMATCH_MINIMUM_TIME_MS // Show the latest content as soon as possible for unlocking to home. && mActivityType != ACTIVITY_TYPE_HOME) { final long delayTime = mShownTime + SIZE_MISMATCH_MINIMUM_TIME_MS - now; - mainExecutor.executeDelayed(() -> remove(mainExecutor), delayTime); + mMainExecutor.executeDelayed(() -> remove(), delayTime); if (DEBUG) { Slog.d(TAG, "Defer removing snapshot surface in " + delayTime); } return; } - mainExecutor.execute(() -> { - try { - if (DEBUG) { - Slog.d(TAG, "Removing snapshot surface, mHasDrawn: " + mHasDrawn); - } - mSession.remove(mWindow); - } catch (RemoteException e) { - // nothing + try { + if (DEBUG) { + Slog.d(TAG, "Removing snapshot surface, mHasDrawn: " + mHasDrawn); } - }); + mSession.remove(mWindow); + } catch (RemoteException e) { + // nothing + } } /** @@ -498,13 +499,12 @@ public class TaskSnapshotWindow { } } + @BinderThread static class Window extends BaseIWindow { private TaskSnapshotWindow mOuter; - private ShellExecutor mMainExecutor; - public void setOuter(TaskSnapshotWindow outer, ShellExecutor mainExecutor) { + public void setOuter(TaskSnapshotWindow outer) { mOuter = outer; - mMainExecutor = mainExecutor; } @Override @@ -512,22 +512,20 @@ public class TaskSnapshotWindow { MergedConfiguration mergedConfiguration, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId) { if (mOuter != null) { - if (mergedConfiguration != null - && mOuter.mOrientationOnCreation - != mergedConfiguration.getMergedConfiguration().orientation) { - // The orientation of the screen is changing. We better remove the snapshot ASAP - // as we are going to wait on the new window in any case to unfreeze the screen, - // and the starting window is not needed anymore. - mMainExecutor.execute(() -> { + mOuter.mMainExecutor.execute(() -> { + if (mergedConfiguration != null + && mOuter.mOrientationOnCreation + != mergedConfiguration.getMergedConfiguration().orientation) { + // The orientation of the screen is changing. We better remove the snapshot + // ASAP as we are going to wait on the new window in any case to unfreeze + // the screen, and the starting window is not needed anymore. mOuter.clearWindowSynced(); - }); - } else if (reportDraw) { - mMainExecutor.execute(() -> { + } else if (reportDraw) { if (mOuter.mHasDrawn) { mOuter.reportDrawn(); } - }); - } + } + }); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java index 8271b0689053..ac93a17b4014 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java @@ -31,7 +31,9 @@ import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.ArrayList; @@ -71,14 +73,20 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { @NonNull Transitions.TransitionFinishCallback finishCallback) { IRemoteTransition pendingRemote = mPendingRemotes.remove(transition); if (pendingRemote == null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s doesn't have " + + "explicit remote, search filters for match for %s", transition, info); // If no explicit remote, search filters until one matches for (int i = mFilters.size() - 1; i >= 0; --i) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Checking filter %s", + mFilters.get(i)); if (mFilters.get(i).first.matches(info)) { pendingRemote = mFilters.get(i).second; break; } } } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Delegate animation for %s to %s", + transition, pendingRemote); if (pendingRemote == null) return false; @@ -121,6 +129,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { IRemoteTransition remote = request.getRemoteTransition(); if (remote == null) return null; mPendingRemotes.put(transition, remote); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "RemoteTransition directly requested" + + " for %s: %s", transition, remote); return new WindowContainerTransaction(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitions.java new file mode 100644 index 000000000000..85bbf74d56b9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitions.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2021 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.wm.shell.transition; + +import android.annotation.NonNull; +import android.window.IRemoteTransition; +import android.window.TransitionFilter; + +import com.android.wm.shell.common.annotations.ExternalThread; + +/** + * Interface to manage remote transitions. + */ +@ExternalThread +public interface RemoteTransitions { + /** + * Registers a remote transition. + */ + void registerRemote(@NonNull TransitionFilter filter, + @NonNull IRemoteTransition remoteTransition); + + /** + * Unregisters a remote transition. + */ + void unregisterRemote(@NonNull IRemoteTransition remoteTransition); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 2ab4e0bdd76f..10344892e766 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -67,6 +67,7 @@ public class Transitions { private final ShellExecutor mAnimExecutor; private final TransitionPlayerImpl mPlayerImpl; private final RemoteTransitionHandler mRemoteTransitionHandler; + private final RemoteTransitionImpl mImpl = new RemoteTransitionImpl(); /** List of possible handlers. Ordered by specificity (eg. tapped back to front). */ private final ArrayList<TransitionHandler> mHandlers = new ArrayList<>(); @@ -78,6 +79,10 @@ public class Transitions { /** Keeps track of currently tracked transitions and all the animations associated with each */ private final ArrayMap<IBinder, ActiveTransition> mActiveTransitions = new ArrayMap<>(); + public static RemoteTransitions asRemoteTransitions(Transitions transitions) { + return transitions.mImpl; + } + public Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { mOrganizer = organizer; @@ -101,8 +106,20 @@ public class Transitions { /** Create an empty/non-registering transitions object for system-ui tests. */ @VisibleForTesting - public static Transitions createEmptyForTesting() { - return new Transitions(); + public static RemoteTransitions createEmptyForTesting() { + return new RemoteTransitions() { + @Override + public void registerRemote(@androidx.annotation.NonNull TransitionFilter filter, + @androidx.annotation.NonNull IRemoteTransition remoteTransition) { + // Do nothing + } + + @Override + public void unregisterRemote( + @androidx.annotation.NonNull IRemoteTransition remoteTransition) { + // Do nothing + } + }; } /** Register this transition handler with Core */ @@ -134,16 +151,14 @@ public class Transitions { } /** Register a remote transition to be used when `filter` matches an incoming transition */ - @ExternalThread public void registerRemote(@NonNull TransitionFilter filter, @NonNull IRemoteTransition remoteTransition) { - mMainExecutor.execute(() -> mRemoteTransitionHandler.addFiltered(filter, remoteTransition)); + mRemoteTransitionHandler.addFiltered(filter, remoteTransition); } /** Unregisters a remote transition and all associated filters */ - @ExternalThread public void unregisterRemote(@NonNull IRemoteTransition remoteTransition) { - mMainExecutor.execute(() -> mRemoteTransitionHandler.removeFiltered(remoteTransition)); + mRemoteTransitionHandler.removeFiltered(remoteTransition); } /** @return true if the transition was triggered by opening something vs closing something */ @@ -232,6 +247,8 @@ public class Transitions { if (!info.getRootLeash().isValid()) { // Invalid root-leash implies that the transition is empty/no-op, so just do // housekeeping and return. + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Invalid root leash (%s): %s", + transitionToken, info); t.apply(); onFinish(transitionToken, null /* wct */, null /* wctCB */); return; @@ -241,14 +258,22 @@ public class Transitions { final TransitionFinishCallback finishCb = (wct, cb) -> onFinish(transitionToken, wct, cb); // If a handler chose to uniquely run this animation, try delegating to it. - if (active.mFirstHandler != null && active.mFirstHandler.startAnimation( - transitionToken, info, t, finishCb)) { - return; + if (active.mFirstHandler != null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try firstHandler %s", + active.mFirstHandler); + if (active.mFirstHandler.startAnimation(transitionToken, info, t, finishCb)) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler"); + return; + } } // Otherwise give every other handler a chance (in order) for (int i = mHandlers.size() - 1; i >= 0; --i) { if (mHandlers.get(i) == active.mFirstHandler) continue; + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try handler %s", + mHandlers.get(i)); if (mHandlers.get(i).startAnimation(transitionToken, info, t, finishCb)) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by %s", + mHandlers.get(i)); return; } } @@ -361,4 +386,22 @@ public class Transitions { mMainExecutor.execute(() -> Transitions.this.requestStartTransition(iBinder, request)); } } + + @ExternalThread + private class RemoteTransitionImpl implements RemoteTransitions { + @Override + public void registerRemote(@NonNull TransitionFilter filter, + @NonNull IRemoteTransition remoteTransition) { + mMainExecutor.execute(() -> { + Transitions.this.registerRemote(filter, remoteTransition); + }); + } + + @Override + public void unregisterRemote(@NonNull IRemoteTransition remoteTransition) { + mMainExecutor.execute(() -> { + Transitions.this.unregisterRemote(remoteTransition); + }); + } + } } diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp index 4a498d2ec581..a57ac35583b2 100644 --- a/libs/WindowManager/Shell/tests/flicker/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/Android.bp @@ -18,7 +18,7 @@ android_test { name: "WMShellFlickerTests", srcs: ["src/**/*.java", "src/**/*.kt"], manifest: "AndroidManifest.xml", - test_config: "AndroidTestPhysicalDevices.xml", + test_config: "AndroidTest.xml", platform_apis: true, certificate: "platform", test_suites: ["device-tests"], @@ -36,25 +36,3 @@ android_test { "wmshell-flicker-test-components", ], } - -android_test { - name: "WMShellFlickerTestsVirtual", - srcs: ["src/**/*.java", "src/**/*.kt"], - manifest: "AndroidManifest.xml", - test_config: "AndroidTestVirtualDevices.xml", - platform_apis: true, - certificate: "platform", - libs: ["android.test.runner"], - static_libs: [ - "androidx.test.ext.junit", - "flickerlib", - "truth-prebuilt", - "app-helpers-core", - "launcher-helper-lib", - "launcher-aosp-tapl", - "wm-flicker-common-assertions", - "wm-flicker-common-app-helpers", - "platform-test-annotations", - "wmshell-flicker-test-components", - ], -} diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestPhysicalDevices.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml index 8258630a9502..f06d57c6c789 100644 --- a/libs/WindowManager/Shell/tests/flicker/AndroidTestPhysicalDevices.xml +++ b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml @@ -25,8 +25,6 @@ </target_preparer> <test class="com.android.tradefed.testtype.AndroidJUnitTest"> <option name="package" value="com.android.wm.shell.flicker"/> - <option name="include-annotation" value="androidx.test.filters.RequiresDevice" /> - <option name="exclude-annotation" value="androidx.test.filters.FlakyTest" /> <option name="shell-timeout" value="6600s" /> <option name="test-timeout" value="6000s" /> <option name="hidden-api-checks" value="false" /> diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestVirtualDevices.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTestVirtualDevices.xml deleted file mode 100644 index 073860875004..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/AndroidTestVirtualDevices.xml +++ /dev/null @@ -1,41 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - * Copyright 2020 Google Inc. All Rights Reserved. - --> -<configuration description="Runs WindowManager Shell Flicker Tests"> - <option name="test-tag" value="FlickerTests" /> - <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> - <!-- keeps the screen on during tests --> - <option name="screen-always-on" value="on" /> - <!-- prevents the phone from restarting --> - <option name="force-skip-system-props" value="true" /> - <!-- set WM tracing verbose level to all --> - <option name="run-command" value="cmd window tracing level all" /> - <!-- inform WM to log all transactions --> - <option name="run-command" value="cmd window tracing transaction" /> - <!-- restart launcher to activate TAPL --> - <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" /> - </target_preparer> - <target_preparer class="com.android.tradefed.targetprep.DeviceCleaner"> - <!-- reboot the device to teardown any crashed tests --> - <option name="cleanup-action" value="REBOOT" /> - </target_preparer> - <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> - <option name="cleanup-apks" value="true"/> - <option name="test-file-name" value="WMShellFlickerTests.apk"/> - <option name="test-file-name" value="WMShellFlickerTestApp.apk" /> - </target_preparer> - <test class="com.android.tradefed.testtype.AndroidJUnitTest"> - <option name="package" value="com.android.wm.shell.flicker"/> - <option name="exclude-annotation" value="androidx.test.filters.RequiresDevice" /> - <option name="exclude-annotation" value="androidx.test.filters.FlakyTest" /> - <option name="shell-timeout" value="6600s" /> - <option name="test-timeout" value="6000s" /> - <option name="hidden-api-checks" value="false" /> - </test> - <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> - <option name="directory-keys" value="/sdcard/flicker" /> - <option name="collect-on-run-ended-only" value="true" /> - <option name="clean-up" value="true" /> - </metrics_collector> -</configuration>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt index ccfdce65f7b2..3282ece999ac 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt @@ -19,44 +19,149 @@ package com.android.wm.shell.flicker import android.graphics.Region import android.view.Surface import com.android.server.wm.flicker.dsl.LayersAssertionBuilder +import com.android.server.wm.flicker.dsl.LayersAssertionBuilderLegacy +import com.android.server.wm.flicker.APP_PAIR_SPLIT_DIVIDER +import com.android.server.wm.flicker.DOCKED_STACK_DIVIDER import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.traces.layers.getVisibleBounds -import com.android.wm.shell.flicker.FlickerTestBase.Companion.DOCKED_STACK_DIVIDER @JvmOverloads -fun LayersAssertionBuilder.appPairsDividerIsVisible( +fun LayersAssertionBuilder.appPairsDividerIsVisible(bugId: Int = 0) { + end("appPairsDividerIsVisible", bugId) { + this.isVisible(APP_PAIR_SPLIT_DIVIDER) + } +} + +@JvmOverloads +fun LayersAssertionBuilder.appPairsDividerIsInvisible(bugId: Int = 0) { + end("appPairsDividerIsInVisible", bugId) { + this.notExists(APP_PAIR_SPLIT_DIVIDER) + } +} + +@JvmOverloads +fun LayersAssertionBuilder.appPairsDividerBecomesVisible(bugId: Int = 0) { + all("dividerLayerBecomesVisible", bugId) { + this.hidesLayer(DOCKED_STACK_DIVIDER) + .then() + .showsLayer(DOCKED_STACK_DIVIDER) + } +} + +@JvmOverloads +fun LayersAssertionBuilder.dockedStackDividerIsVisible(bugId: Int = 0) { + end("dockedStackDividerIsVisible", bugId) { + this.isVisible(DOCKED_STACK_DIVIDER) + } +} + +@JvmOverloads +fun LayersAssertionBuilder.dockedStackDividerBecomesVisible(bugId: Int = 0) { + all("dividerLayerBecomesVisible", bugId) { + this.hidesLayer(DOCKED_STACK_DIVIDER) + .then() + .showsLayer(DOCKED_STACK_DIVIDER) + } +} + +@JvmOverloads +fun LayersAssertionBuilder.dockedStackDividerBecomesInvisible(bugId: Int = 0) { + all("dividerLayerBecomesInvisible", bugId) { + this.showsLayer(DOCKED_STACK_DIVIDER) + .then() + .hidesLayer(DOCKED_STACK_DIVIDER) + } +} + +@JvmOverloads +fun LayersAssertionBuilder.dockedStackDividerIsInvisible(bugId: Int = 0) { + end("dockedStackDividerIsInvisible", bugId) { + this.notExists(DOCKED_STACK_DIVIDER) + } +} + +@JvmOverloads +fun LayersAssertionBuilder.appPairsPrimaryBoundsIsVisible( + rotation: Int, + primaryLayerName: String, + bugId: Int = 0 +) { + end("PrimaryAppBounds", bugId) { + val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) + this.hasVisibleRegion(primaryLayerName, getPrimaryRegion(dividerRegion, rotation)) + } +} + +@JvmOverloads +fun LayersAssertionBuilder.appPairsSecondaryBoundsIsVisible( + rotation: Int, + secondaryLayerName: String, + bugId: Int = 0 +) { + end("SecondaryAppBounds", bugId) { + val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) + this.hasVisibleRegion(secondaryLayerName, getSecondaryRegion(dividerRegion, rotation)) + } +} + +@JvmOverloads +fun LayersAssertionBuilder.dockedStackPrimaryBoundsIsVisible( + rotation: Int, + primaryLayerName: String, + bugId: Int = 0 +) { + end("PrimaryAppBounds", bugId) { + val dividerRegion = entry.getVisibleBounds(DOCKED_STACK_DIVIDER) + this.hasVisibleRegion(primaryLayerName, getPrimaryRegion(dividerRegion, rotation)) + } +} + +@JvmOverloads +fun LayersAssertionBuilder.dockedStackSecondaryBoundsIsVisible( + rotation: Int, + secondaryLayerName: String, + bugId: Int = 0 +) { + end("SecondaryAppBounds", bugId) { + val dividerRegion = entry.getVisibleBounds(DOCKED_STACK_DIVIDER) + this.hasVisibleRegion(secondaryLayerName, getSecondaryRegion(dividerRegion, rotation)) + } +} + +@JvmOverloads +fun LayersAssertionBuilderLegacy.appPairsDividerIsVisible( bugId: Int = 0, enabled: Boolean = bugId == 0 ) { end("appPairsDividerIsVisible", bugId, enabled) { - this.isVisible(FlickerTestBase.APP_PAIR_SPLIT_DIVIDER) + this.isVisible(APP_PAIR_SPLIT_DIVIDER) } } @JvmOverloads -fun LayersAssertionBuilder.appPairsDividerIsInvisible( +fun LayersAssertionBuilderLegacy.appPairsDividerIsInvisible( bugId: Int = 0, enabled: Boolean = bugId == 0 ) { end("appPairsDividerIsInVisible", bugId, enabled) { - this.notExists(FlickerTestBase.APP_PAIR_SPLIT_DIVIDER) + this.notExists(APP_PAIR_SPLIT_DIVIDER) } } @JvmOverloads -fun LayersAssertionBuilder.appPairsDividerBecomesVisible( +fun LayersAssertionBuilderLegacy.appPairsDividerBecomesVisible( bugId: Int = 0, enabled: Boolean = bugId == 0 ) { all("dividerLayerBecomesVisible", bugId, enabled) { this.hidesLayer(DOCKED_STACK_DIVIDER) - .then() - .showsLayer(DOCKED_STACK_DIVIDER) + .then() + .showsLayer(DOCKED_STACK_DIVIDER) } } @JvmOverloads -fun LayersAssertionBuilder.dockedStackDividerIsVisible( +fun LayersAssertionBuilderLegacy.dockedStackDividerIsVisible( bugId: Int = 0, enabled: Boolean = bugId == 0 ) { @@ -66,31 +171,31 @@ fun LayersAssertionBuilder.dockedStackDividerIsVisible( } @JvmOverloads -fun LayersAssertionBuilder.dockedStackDividerBecomesVisible( +fun LayersAssertionBuilderLegacy.dockedStackDividerBecomesVisible( bugId: Int = 0, enabled: Boolean = bugId == 0 ) { all("dividerLayerBecomesVisible", bugId, enabled) { this.hidesLayer(DOCKED_STACK_DIVIDER) - .then() - .showsLayer(DOCKED_STACK_DIVIDER) + .then() + .showsLayer(DOCKED_STACK_DIVIDER) } } @JvmOverloads -fun LayersAssertionBuilder.dockedStackDividerBecomesInvisible( +fun LayersAssertionBuilderLegacy.dockedStackDividerBecomesInvisible( bugId: Int = 0, enabled: Boolean = bugId == 0 ) { all("dividerLayerBecomesInvisible", bugId, enabled) { this.showsLayer(DOCKED_STACK_DIVIDER) - .then() - .hidesLayer(DOCKED_STACK_DIVIDER) + .then() + .hidesLayer(DOCKED_STACK_DIVIDER) } } @JvmOverloads -fun LayersAssertionBuilder.dockedStackDividerIsInvisible( +fun LayersAssertionBuilderLegacy.dockedStackDividerIsInvisible( bugId: Int = 0, enabled: Boolean = bugId == 0 ) { @@ -100,33 +205,33 @@ fun LayersAssertionBuilder.dockedStackDividerIsInvisible( } @JvmOverloads -fun LayersAssertionBuilder.appPairsPrimaryBoundsIsVisible( +fun LayersAssertionBuilderLegacy.appPairsPrimaryBoundsIsVisible( rotation: Int, primaryLayerName: String, bugId: Int = 0, enabled: Boolean = bugId == 0 ) { end("PrimaryAppBounds", bugId, enabled) { - val dividerRegion = entry.getVisibleBounds(FlickerTestBase.APP_PAIR_SPLIT_DIVIDER) + val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) this.hasVisibleRegion(primaryLayerName, getPrimaryRegion(dividerRegion, rotation)) } } @JvmOverloads -fun LayersAssertionBuilder.appPairsSecondaryBoundsIsVisible( +fun LayersAssertionBuilderLegacy.appPairsSecondaryBoundsIsVisible( rotation: Int, secondaryLayerName: String, bugId: Int = 0, enabled: Boolean = bugId == 0 ) { end("SecondaryAppBounds", bugId, enabled) { - val dividerRegion = entry.getVisibleBounds(FlickerTestBase.APP_PAIR_SPLIT_DIVIDER) + val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) this.hasVisibleRegion(secondaryLayerName, getSecondaryRegion(dividerRegion, rotation)) } } @JvmOverloads -fun LayersAssertionBuilder.dockedStackPrimaryBoundsIsVisible( +fun LayersAssertionBuilderLegacy.dockedStackPrimaryBoundsIsVisible( rotation: Int, primaryLayerName: String, bugId: Int = 0, @@ -139,7 +244,7 @@ fun LayersAssertionBuilder.dockedStackPrimaryBoundsIsVisible( } @JvmOverloads -fun LayersAssertionBuilder.dockedStackSecondaryBoundsIsVisible( +fun LayersAssertionBuilderLegacy.dockedStackSecondaryBoundsIsVisible( rotation: Int, secondaryLayerName: String, bugId: Int = 0, diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/Extensions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/Extensions.kt new file mode 100644 index 000000000000..1869d833fbc4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/Extensions.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 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. + */ + +@file:JvmName("Utils") +package com.android.wm.shell.flicker + +import android.app.ActivityTaskManager +import android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT +import android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED + +fun removeAllTasksButHome() { + val ALL_ACTIVITY_TYPE_BUT_HOME = intArrayOf( + ACTIVITY_TYPE_STANDARD, ACTIVITY_TYPE_ASSISTANT, ACTIVITY_TYPE_RECENTS, + ACTIVITY_TYPE_UNDEFINED) + val atm = ActivityTaskManager.getService() + atm.removeRootTasksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME) +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt index 3953c1c5a0f4..89bbdb0a2f99 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt @@ -135,12 +135,4 @@ abstract class FlickerTestBase { throw RuntimeException(e) } } - - companion object { - const val NAVIGATION_BAR_WINDOW_TITLE = "NavigationBar" - const val STATUS_BAR_WINDOW_TITLE = "StatusBar" - const val DOCKED_STACK_DIVIDER = "DockedStackDivider" - const val APP_PAIR_SPLIT_DIVIDER = "AppPairSplitDivider" - const val IMAGE_WALLPAPER = "ImageWallpaper" - } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt index d25774935e86..c3fd66395366 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.apppairs import android.os.Bundle -import android.platform.test.annotations.Presubmit import android.os.SystemClock import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry @@ -40,7 +39,6 @@ import org.junit.runners.Parameterized * Test cold launch app from launcher. * To run this test: `atest WMShellFlickerTests:AppPairsTestCannotPairNonResizeableApps` */ -@Presubmit @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @@ -51,10 +49,9 @@ class AppPairsTestCannotPairNonResizeableApps( @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): List<Array<Any>> { - val testTag = "testAppPairs_cannotPairNonResizeableApps" val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> withTestName { - buildTestTag(testTag, configuration) + buildTestTag(configuration) } transitions { nonResizeableApp?.launchViaIntent(wmHelper) @@ -64,17 +61,20 @@ class AppPairsTestCannotPairNonResizeableApps( SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) } assertions { - layersTrace { - appPairsDividerIsInvisible() - } - windowManagerTrace { - end("onlyResizeableAppWindowVisible") { + presubmit { + layersTrace { + appPairsDividerIsInvisible() + } + windowManagerTrace { val nonResizeableApp = nonResizeableApp require(nonResizeableApp != null) { "Non resizeable app not initialized" } - isVisible(nonResizeableApp.defaultWindowName) - isInvisible(primaryApp.defaultWindowName) + + end("onlyResizeableAppWindowVisible") { + isVisible(nonResizeableApp.defaultWindowName) + isInvisible(primaryApp.defaultWindowName) + } } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt index 5cbfec638da5..7a2a5e482d98 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt @@ -18,15 +18,14 @@ package com.android.wm.shell.flicker.apppairs import android.os.Bundle import android.os.SystemClock -import android.platform.test.annotations.Presubmit import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.APP_PAIR_SPLIT_DIVIDER import com.android.server.wm.flicker.FlickerTestRunner import com.android.server.wm.flicker.FlickerTestRunnerFactory import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.buildTestTag import com.android.server.wm.flicker.traces.layers.getVisibleBounds -import com.android.wm.shell.flicker.FlickerTestBase.Companion.APP_PAIR_SPLIT_DIVIDER import com.android.wm.shell.flicker.appPairsDividerIsVisible import com.android.wm.shell.flicker.helpers.AppPairsHelper import org.junit.FixMethodOrder @@ -38,7 +37,6 @@ import org.junit.runners.Parameterized * Test cold launch app from launcher. * To run this test: `atest WMShellFlickerTests:AppPairsTestPairPrimaryAndSecondaryApps` */ -@Presubmit @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @@ -49,10 +47,9 @@ class AppPairsTestPairPrimaryAndSecondaryApps( @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): List<Array<Any>> { - val testTag = "testAppPairs_pairPrimaryAndSecondaryApps" val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> withTestName { - buildTestTag(testTag, configuration) + buildTestTag(configuration) } transitions { // TODO pair apps through normal UX flow @@ -61,20 +58,27 @@ class AppPairsTestPairPrimaryAndSecondaryApps( SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) } assertions { - layersTrace { - appPairsDividerIsVisible() - end("appsEndingBounds", enabled = false) { - val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) - this.hasVisibleRegion(primaryApp.defaultWindowName, - appPairsHelper.getPrimaryBounds(dividerRegion)) - .hasVisibleRegion(secondaryApp.defaultWindowName, - appPairsHelper.getSecondaryBounds(dividerRegion)) + presubmit { + layersTrace { + appPairsDividerIsVisible() + } + windowManagerTrace { + end("bothAppWindowsVisible") { + isVisible(primaryApp.defaultWindowName) + isVisible(secondaryApp.defaultWindowName) + } } } - windowManagerTrace { - end("bothAppWindowsVisible") { - isVisible(primaryApp.defaultWindowName) - isVisible(secondaryApp.defaultWindowName) + + flaky { + layersTrace { + end("appsEndingBounds") { + val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) + this.hasVisibleRegion(primaryApp.defaultWindowName, + appPairsHelper.getPrimaryBounds(dividerRegion)) + .hasVisibleRegion(secondaryApp.defaultWindowName, + appPairsHelper.getSecondaryBounds(dividerRegion)) + } } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt index f57a000a0ccb..d8dc4c2b56f6 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt @@ -18,15 +18,14 @@ package com.android.wm.shell.flicker.apppairs import android.os.Bundle import android.os.SystemClock -import android.platform.test.annotations.Presubmit import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.APP_PAIR_SPLIT_DIVIDER import com.android.server.wm.flicker.FlickerTestRunner import com.android.server.wm.flicker.FlickerTestRunnerFactory import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.buildTestTag import com.android.server.wm.flicker.traces.layers.getVisibleBounds -import com.android.wm.shell.flicker.FlickerTestBase.Companion.APP_PAIR_SPLIT_DIVIDER import com.android.wm.shell.flicker.appPairsDividerIsInvisible import com.android.wm.shell.flicker.helpers.AppPairsHelper import org.junit.FixMethodOrder @@ -38,7 +37,6 @@ import org.junit.runners.Parameterized * Test cold launch app from launcher. * To run this test: `atest WMShellFlickerTests:AppPairsTestUnpairPrimaryAndSecondaryApps` */ -@Presubmit @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @@ -49,10 +47,9 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): List<Array<Any>> { - val testTag = "testAppPairs_unpairPrimaryAndSecondaryApps" val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> withTestName { - buildTestTag(testTag, configuration) + buildTestTag(configuration) } setup { executeShellCommand( @@ -66,24 +63,31 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) } assertions { - layersTrace { - appPairsDividerIsInvisible() - start("appsStartingBounds", enabled = false) { - val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) - this.hasVisibleRegion(primaryApp.defaultWindowName, - appPairsHelper.getPrimaryBounds(dividerRegion)) - .hasVisibleRegion(secondaryApp.defaultWindowName, - appPairsHelper.getSecondaryBounds(dividerRegion)) + presubmit { + layersTrace { + appPairsDividerIsInvisible() } - end("appsEndingBounds", enabled = false) { - this.notExists(primaryApp.defaultWindowName) - .notExists(secondaryApp.defaultWindowName) + windowManagerTrace { + end("bothAppWindowsInvisible") { + isInvisible(primaryApp.defaultWindowName) + isInvisible(secondaryApp.defaultWindowName) + } } } - windowManagerTrace { - end("bothAppWindowsInvisible") { - isInvisible(primaryApp.defaultWindowName) - isInvisible(secondaryApp.defaultWindowName) + + flaky { + layersTrace { + start("appsStartingBounds") { + val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) + this.hasVisibleRegion(primaryApp.defaultWindowName, + appPairsHelper.getPrimaryBounds(dividerRegion)) + .hasVisibleRegion(secondaryApp.defaultWindowName, + appPairsHelper.getSecondaryBounds(dividerRegion)) + } + end("appsEndingBounds") { + this.notExists(primaryApp.defaultWindowName) + .notExists(secondaryApp.defaultWindowName) + } } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt index 7191e8e95590..8aee005b7513 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt @@ -18,7 +18,6 @@ package com.android.wm.shell.flicker.apppairs import android.os.Bundle import android.os.SystemClock -import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry @@ -27,11 +26,9 @@ import com.android.server.wm.flicker.FlickerTestRunnerFactory import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.buildTestTag -import com.android.server.wm.flicker.helpers.isRotated import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible import com.android.wm.shell.flicker.appPairsDividerIsVisible @@ -48,7 +45,6 @@ import org.junit.runners.Parameterized * Test open apps to app pairs and rotate. * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppsInAppPairsMode` */ -@Presubmit @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @@ -62,7 +58,7 @@ class RotateTwoLaunchedAppsInAppPairsMode( fun getParams(): Collection<Array<Any>> { val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> withTestName { - buildTestTag("testRotateTwoLaunchedAppsInAppPairsMode", configuration) + buildTestTag(configuration) } transitions { executeShellCommand(composePairsCommand( @@ -71,22 +67,28 @@ class RotateTwoLaunchedAppsInAppPairsMode( setRotation(configuration.endRotation) } assertions { - layersTrace { - navBarLayerRotatesAndScales(Surface.ROTATION_0, configuration.endRotation, - enabled = !configuration.startRotation.isRotated()) - statusBarLayerRotatesScales(Surface.ROTATION_0, configuration.endRotation) - appPairsDividerIsVisible() - appPairsPrimaryBoundsIsVisible(configuration.endRotation, - primaryApp.defaultWindowName, 172776659) - appPairsSecondaryBoundsIsVisible(configuration.endRotation, - secondaryApp.defaultWindowName, 172776659) + presubmit { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + end("bothAppWindowsVisible") { + isVisible(primaryApp.defaultWindowName) + .isVisible(secondaryApp.defaultWindowName) + } + } } - windowManagerTrace { - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - end("bothAppWindowsVisible") { - isVisible(primaryApp.defaultWindowName) - .isVisible(secondaryApp.defaultWindowName) + + flaky { + layersTrace { + appPairsDividerIsVisible() + navBarLayerRotatesAndScales(Surface.ROTATION_0, + configuration.endRotation) + statusBarLayerRotatesScales(Surface.ROTATION_0, + configuration.endRotation) + appPairsPrimaryBoundsIsVisible(configuration.endRotation, + primaryApp.defaultWindowName, bugId = 172776659) + appPairsSecondaryBoundsIsVisible(configuration.endRotation, + secondaryApp.defaultWindowName, bugId = 172776659) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt index 19ca31fbee4a..bc99c9430f13 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt @@ -62,7 +62,7 @@ class RotateTwoLaunchedAppsRotateAndEnterAppPairsMode( fun getParams(): Collection<Array<Any>> { val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> withTestName { - buildTestTag("testRotateAndEnterAppPairsMode", configuration) + buildTestTag(configuration) } transitions { this.setRotation(configuration.endRotation) @@ -71,22 +71,37 @@ class RotateTwoLaunchedAppsRotateAndEnterAppPairsMode( SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) } assertions { - layersTrace { - navBarLayerRotatesAndScales(Surface.ROTATION_0, configuration.endRotation, - enabled = !configuration.startRotation.isRotated()) - statusBarLayerRotatesScales(Surface.ROTATION_0, configuration.endRotation) - appPairsDividerIsVisible() - appPairsPrimaryBoundsIsVisible(configuration.endRotation, - primaryApp.defaultWindowName, 172776659) - appPairsSecondaryBoundsIsVisible(configuration.endRotation, - secondaryApp.defaultWindowName, 172776659) + val isRotated = configuration.startRotation.isRotated() + presubmit { + layersTrace { + statusBarLayerRotatesScales(Surface.ROTATION_0, + configuration.endRotation) + appPairsDividerIsVisible() + if (!isRotated) { + navBarLayerRotatesAndScales(Surface.ROTATION_0, + configuration.endRotation) + } + } + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + end("bothAppWindowsVisible") { + isVisible(primaryApp.defaultWindowName) + isVisible(secondaryApp.defaultWindowName) + } + } } - windowManagerTrace { - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - end("bothAppWindowsVisible") { - isVisible(primaryApp.defaultWindowName) - isVisible(secondaryApp.defaultWindowName) + flaky { + layersTrace { + appPairsPrimaryBoundsIsVisible(configuration.endRotation, + primaryApp.defaultWindowName, 172776659) + appPairsSecondaryBoundsIsVisible(configuration.endRotation, + secondaryApp.defaultWindowName, 172776659) + + if (isRotated) { + navBarLayerRotatesAndScales(Surface.ROTATION_0, + configuration.endRotation) + } } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt index 7ec22bb9db1c..cac46fe676b3 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt @@ -32,13 +32,12 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( /** * Opens the IME and wait for it to be displayed * - * @param device UIDevice instance to interact with the device * @param wmHelper Helper used to wait for WindowManager states */ @JvmOverloads - open fun openIME(device: UiDevice, wmHelper: WindowManagerStateHelper? = null) { + open fun openIME(wmHelper: WindowManagerStateHelper? = null) { if (!isTelevision) { - val editText = device.wait( + val editText = uiDevice.wait( Until.findObject(By.res(getPackage(), "plain_text_input")), FIND_TIMEOUT) @@ -47,7 +46,7 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( "was left in an unknown state (e.g. in split screen)" } editText.click() - waitAndAssertIMEShown(device, wmHelper) + waitAndAssertIMEShown(uiDevice, wmHelper) } else { // If we do the same thing as above - editText.click() - on TV, that's going to force TV // into the touch mode. We really don't want that. @@ -69,16 +68,15 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( /** * Opens the IME and wait for it to be gone * - * @param device UIDevice instance to interact with the device * @param wmHelper Helper used to wait for WindowManager states */ @JvmOverloads - open fun closeIME(device: UiDevice, wmHelper: WindowManagerStateHelper? = null) { + open fun closeIME(wmHelper: WindowManagerStateHelper? = null) { if (!isTelevision) { - device.pressBack() + uiDevice.pressBack() // Using only the AccessibilityInfo it is not possible to identify if the IME is active if (wmHelper == null) { - device.waitForIdle() + uiDevice.waitForIdle() } else { require(wmHelper.waitImeWindowGone()) { "IME did did not close" } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt index b90e865de691..ecc066be734f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt @@ -17,17 +17,19 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation +import android.graphics.Point import android.media.session.MediaController import android.media.session.MediaSessionManager import android.os.SystemClock import androidx.test.uiautomator.By import androidx.test.uiautomator.BySelector -import com.android.server.wm.flicker.helpers.closePipWindow -import com.android.server.wm.flicker.helpers.hasPipWindow +import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE +import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import com.android.wm.shell.flicker.pip.tv.closeTvPipWindow import com.android.wm.shell.flicker.pip.tv.isFocusedOrHasFocusedChild +import com.android.wm.shell.flicker.pip.waitPipWindowGone +import com.android.wm.shell.flicker.pip.waitPipWindowShown import com.android.wm.shell.flicker.testapp.Components -import org.junit.Assert.fail class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( instrumentation, @@ -55,6 +57,17 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( } } + /** {@inheritDoc} */ + override fun launchViaIntent( + wmHelper: WindowManagerStateHelper, + expectedWindowName: String, + action: String?, + stringExtras: Map<String, String> + ) { + super.launchViaIntent(wmHelper, expectedWindowName, action, stringExtras) + wmHelper.waitPipWindowShown() + } + private fun focusOnObject(selector: BySelector): Boolean { // We expect all the focusable UI elements to be arranged in a way so that it is possible // to "cycle" over all them by clicking the D-Pad DOWN button, going back up to "the top" @@ -69,16 +82,12 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( return false } - fun clickEnterPipButton() { + @JvmOverloads + fun clickEnterPipButton(wmHelper: WindowManagerStateHelper? = null) { clickObject(ENTER_PIP_BUTTON_ID) - // TODO(b/172321238): remove this check once hasPipWindow is fixed on TVs - if (!isTelevision) { - uiDevice.hasPipWindow() - } else { - // Simply wait for 3 seconds - SystemClock.sleep(3_000) - } + // Wait on WMHelper or simply wait for 3 seconds + wmHelper?.waitPipWindowShown() ?: SystemClock.sleep(3_000) } fun clickStartMediaSessionButton() { @@ -97,16 +106,75 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( fun stopMedia() = mediaController?.transportControls?.stop() ?: error("No active media session found") + @Deprecated("Use PipAppHelper.closePipWindow(wmHelper) instead", + ReplaceWith("closePipWindow(wmHelper)")) fun closePipWindow() { if (isTelevision) { uiDevice.closeTvPipWindow() } else { - uiDevice.closePipWindow() + closePipWindow(WindowManagerStateHelper(mInstrumentation)) + } + } + + /** + * Expands the pip window and dismisses it by clicking on the X button. + * + * Note, currently the View coordinates reported by the accessibility are relative to + * the window, so the correct coordinates need to be calculated + * + * For example, in a PIP window located at Rect(508, 1444 - 1036, 1741), the + * dismiss button coordinates are shown as Rect(650, 0 - 782, 132), with center in + * Point(716, 66), instead of Point(970, 1403) + * + * See b/179337864 + */ + fun closePipWindow(wmHelper: WindowManagerStateHelper) { + if (isTelevision) { + uiDevice.closeTvPipWindow() + } else { + expandPipWindow(wmHelper) + val exitPipObject = uiDevice.findObject(By.res(SYSTEMUI_PACKAGE, "dismiss")) + requireNotNull(exitPipObject) { "PIP window dismiss button not found" } + val coordinatesInWindow = exitPipObject.visibleBounds + val windowOffset = wmHelper.getWindowRegion(component).bounds + val newCoordinates = Point(windowOffset.left + coordinatesInWindow.centerX(), + windowOffset.top + coordinatesInWindow.centerY()) + uiDevice.click(newCoordinates.x, newCoordinates.y) + } + + // Wait for animation to complete. + wmHelper.waitPipWindowGone() + wmHelper.waitForHomeActivityVisible() + } + + /** + * Click once on the PIP window to expand it + */ + fun expandPipWindow(wmHelper: WindowManagerStateHelper) { + val windowRegion = wmHelper.getWindowRegion(component) + require(!windowRegion.isEmpty) { + "Unable to find a PIP window in the current state" } + val windowRect = windowRegion.bounds + uiDevice.click(windowRect.centerX(), windowRect.centerY()) + // Ensure WindowManagerService wait until all animations have completed + wmHelper.waitForAppTransitionIdle() + mInstrumentation.uiAutomation.syncInputTransactions() + } - if (!waitUntilClosed()) { - fail("Couldn't close Pip") + /** + * Double click on the PIP window to reopen to app + */ + fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) { + val windowRegion = wmHelper.getWindowRegion(component) + require(!windowRegion.isEmpty) { + "Unable to find a PIP window in the current state" } + val windowRect = windowRegion.bounds + uiDevice.click(windowRect.centerX(), windowRect.centerY()) + uiDevice.click(windowRect.centerX(), windowRect.centerY()) + wmHelper.waitPipWindowGone() + wmHelper.waitForAppTransitionIdle() } companion object { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterLegacySplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterLegacySplitScreenTest.kt deleted file mode 100644 index dea5c300f590..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterLegacySplitScreenTest.kt +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright (C) 2020 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.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.dsl.runWithFlicker -import com.android.server.wm.flicker.helpers.canSplitScreen -import com.android.server.wm.flicker.helpers.exitSplitScreen -import com.android.server.wm.flicker.helpers.isInSplitScreen -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.openQuickstep -import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.wm.shell.flicker.dockedStackDividerIsInvisible -import com.android.wm.shell.flicker.helpers.SplitScreenHelper.Companion.TEST_REPETITIONS -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible -import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible -import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible -import org.junit.Assert -import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible -import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry -import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test SplitScreen launch. - * To run this test: `atest WMShellFlickerTests:EnterLegacySplitScreenTest` - */ -@Presubmit -@RequiresDevice -@RunWith(Parameterized::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class EnterLegacySplitScreenTest( - rotationName: String, - rotation: Int -) : SplitScreenTestBase(rotationName, rotation) { - private val splitScreenSetup: FlickerBuilder - get() = FlickerBuilder(instrumentation).apply { - val testLaunchActivity = "launch_splitScreen_test_activity" - withTestName { - testLaunchActivity - } - setup { - eachRun { - uiDevice.wakeUpAndGoToHomeScreen() - uiDevice.openQuickStepAndClearRecentAppsFromOverview() - } - } - teardown { - eachRun { - if (uiDevice.isInSplitScreen()) { - uiDevice.exitSplitScreen() - } - splitScreenApp.exit() - secondaryApp.exit() - nonResizeableApp.exit() - } - } - assertions { - layersTrace { - navBarLayerIsAlwaysVisible() - statusBarLayerIsAlwaysVisible() - } - windowManagerTrace { - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - } - } - } - - @Test - fun testEnterSplitScreen_dockActivity() { - val testTag = "testEnterSplitScreen_dockActivity" - runWithFlicker(splitScreenSetup) { - withTestName { testTag } - repeat { - TEST_REPETITIONS - } - transitions { - splitScreenApp.launchViaIntent() - uiDevice.launchSplitScreen() - } - assertions { - layersTrace { - dockedStackPrimaryBoundsIsVisible( - rotation, splitScreenApp.defaultWindowName, 169271943) - dockedStackDividerBecomesVisible() - visibleLayersShownMoreThanOneConsecutiveEntry( - listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, - LIVE_WALLPAPER_PACKAGE_NAME) - ) - } - windowManagerTrace { - end("appWindowIsVisible") { - isVisible(splitScreenApp.defaultWindowName) - } - } - } - } - } - - @Test - fun testEnterSplitScreen_launchToSide() { - val testTag = "testEnterSplitScreen_launchToSide" - runWithFlicker(splitScreenSetup) { - withTestName { testTag } - repeat { - TEST_REPETITIONS - } - transitions { - secondaryApp.launchViaIntent() - splitScreenApp.launchViaIntent() - uiDevice.launchSplitScreen() - splitScreenApp.reopenAppFromOverview() - } - assertions { - layersTrace { - dockedStackPrimaryBoundsIsVisible( - rotation, splitScreenApp.defaultWindowName, 169271943) - dockedStackSecondaryBoundsIsVisible( - rotation, secondaryApp.defaultWindowName, 169271943) - dockedStackDividerBecomesVisible() - visibleLayersShownMoreThanOneConsecutiveEntry( - listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, - secondaryApp.defaultWindowName) - ) - } - windowManagerTrace { - end("appWindowIsVisible") { - isVisible(splitScreenApp.defaultWindowName) - .isVisible(secondaryApp.defaultWindowName) - } - visibleWindowsShownMoreThanOneConsecutiveEntry( - listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, - secondaryApp.defaultWindowName)) - } - } - } - } - - @FlakyTest(bugId = 173875043) - @Test - fun testNonResizeableNotDocked() { - val testTag = "testNonResizeableNotDocked" - runWithFlicker(splitScreenSetup) { - withTestName { testTag } - repeat { - TEST_REPETITIONS - } - transitions { - nonResizeableApp.launchViaIntent() - uiDevice.openQuickstep() - if (uiDevice.canSplitScreen()) { - Assert.fail("Non-resizeable app should not enter split screen") - } - } - assertions { - layersTrace { - dockedStackDividerIsInvisible() - visibleLayersShownMoreThanOneConsecutiveEntry( - listOf(LAUNCHER_PACKAGE_NAME, nonResizeableApp.defaultWindowName) - ) - } - windowManagerTrace { - end("appWindowIsVisible") { - isInvisible(nonResizeableApp.defaultWindowName) - } - visibleWindowsShownMoreThanOneConsecutiveEntry( - listOf(LAUNCHER_PACKAGE_NAME, nonResizeableApp.defaultWindowName)) - } - } - } - } - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<Array<Any>> { - val supportedRotations = intArrayOf(Surface.ROTATION_0) - return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt new file mode 100644 index 000000000000..2c29220bf20e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 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.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.WALLPAPER_TITLE +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open activity and dock to primary split screen + * To run this test: `atest WMShellFlickerTests:EnterSplitScreenDockActivity` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +// @FlakyTest(bugId = 179116910) +class EnterSplitScreenDockActivity( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testLegacySplitScreenDockActivity", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + device.launchSplitScreen() + } + assertions { + layersTrace { + dockedStackPrimaryBoundsIsVisible( + configuration.startRotation, + splitScreenApp.defaultWindowName, bugId = 169271943) + dockedStackDividerBecomesVisible() + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, + WALLPAPER_TITLE, LIVE_WALLPAPER_PACKAGE_NAME, + splitScreenApp.defaultWindowName), + bugId = 178531736 + ) + } + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, + WALLPAPER_TITLE, LIVE_WALLPAPER_PACKAGE_NAME, + splitScreenApp.defaultWindowName), + bugId = 178531736 + ) + end("appWindowIsVisible") { + isVisible(splitScreenApp.defaultWindowName) + } + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, defaultTransitionSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0) // bugId = 179116910 + ) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt new file mode 100644 index 000000000000..903971ea084f --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2021 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.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open activity to primary split screen and dock secondary activity to side + * To run this test: `atest WMShellFlickerTests:EnterSplitScreenLaunchToSide` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EnterSplitScreenLaunchToSide( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testLegacySplitScreenLaunchToSide", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + device.launchSplitScreen() + secondaryApp.reopenAppFromOverview() + } + assertions { + layersTrace { + dockedStackPrimaryBoundsIsVisible( + configuration.startRotation, + splitScreenApp.defaultWindowName, bugId = 169271943) + dockedStackSecondaryBoundsIsVisible( + configuration.startRotation, + secondaryApp.defaultWindowName, bugId = 169271943) + dockedStackDividerBecomesVisible() + // TODO(b/178447631) Remove Splash Screen from white list when flicker lib + // add a wait for splash screen be gone + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, SPLASH_SCREEN_NAME, + splitScreenApp.defaultWindowName, + secondaryApp.defaultWindowName), + bugId = 178447631 + ) + } + windowManagerTrace { + appWindowBecomesVisible(secondaryApp.defaultWindowName) + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, SPLASH_SCREEN_NAME, + splitScreenApp.defaultWindowName, + secondaryApp.defaultWindowName) + ) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, defaultTransitionSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0) // bugId = 175687842 + ) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNonResizableNotDock.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNonResizableNotDock.kt new file mode 100644 index 000000000000..e3619235ee77 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNonResizableNotDock.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2021 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.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.FlakyTest +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.WALLPAPER_TITLE +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.canSplitScreen +import com.android.server.wm.flicker.helpers.openQuickstep +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.wm.shell.flicker.dockedStackDividerIsInvisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open non-resizable activity will auto exit split screen mode + * To run this test: `atest WMShellFlickerTests:EnterSplitScreenNonResizableNotDock` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest(bugId = 173875043) +class EnterSplitScreenNonResizableNotDock( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testLegacySplitScreenNonResizeableActivityNotDock", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + nonResizeableApp.launchViaIntent(wmHelper) + device.openQuickstep() + if (device.canSplitScreen()) { + Assert.fail("Non-resizeable app should not enter split screen") + } + } + assertions { + layersTrace { + dockedStackDividerIsInvisible() + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, + SPLASH_SCREEN_NAME, + nonResizeableApp.defaultWindowName, + splitScreenApp.defaultWindowName), + bugId = 178447631 + ) + } + windowManagerTrace { + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(WALLPAPER_TITLE, + LAUNCHER_PACKAGE_NAME, + SPLASH_SCREEN_NAME, + nonResizeableApp.defaultWindowName, + splitScreenApp.defaultWindowName) + ) + end("appWindowIsVisible") { + isInvisible(nonResizeableApp.defaultWindowName) + } + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, defaultTransitionSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0 /* bugId = 178685668 */)) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt new file mode 100644 index 000000000000..493366553623 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2020 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.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesInVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.exitSplitScreenFromBottom +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.layerBecomesInvisible +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open resizeable activity split in primary, and drag divider to bottom exit split screen + * To run this test: `atest WMShellFlickerTests:ExitLegacySplitScreenFromBottom` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExitLegacySplitScreenFromBottom( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testExitLegacySplitScreenFromBottom", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + device.launchSplitScreen() + device.exitSplitScreenFromBottom() + } + assertions { + layersTrace { + layerBecomesInvisible(DOCKED_STACK_DIVIDER) + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, + secondaryApp.defaultWindowName), + bugId = 178447631 + ) + } + windowManagerTrace { + appWindowBecomesInVisible(secondaryApp.defaultWindowName) + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, + secondaryApp.defaultWindowName), + bugId = 178447631 + ) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, defaultTransitionSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0) // bugId = 175687842 + ) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottomTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottomTest.kt deleted file mode 100644 index a3b8673d93ed..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottomTest.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2020 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.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.RequiresDevice -import androidx.test.platform.app.InstrumentationRegistry -import com.android.server.wm.flicker.FlickerTestRunner -import com.android.server.wm.flicker.FlickerTestRunnerFactory -import com.android.server.wm.flicker.endRotation -import com.android.server.wm.flicker.helpers.buildTestTag -import com.android.server.wm.flicker.helpers.exitSplitScreen -import com.android.server.wm.flicker.helpers.exitSplitScreenFromBottom -import com.android.server.wm.flicker.helpers.isInSplitScreen -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.repetitions -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test open app to split screen. - * To run this test: `atest WMShellFlickerTests:ExitLegacySplitScreenFromBottomTest` - */ -@Presubmit -@RequiresDevice -@RunWith(Parameterized::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class ExitLegacySplitScreenFromBottomTest( - testSpec: FlickerTestRunnerFactory.TestSpec -) : FlickerTestRunner(testSpec) { - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<Array<Any>> { - val instrumentation = InstrumentationRegistry.getInstrumentation() - val splitScreenApp = SplitScreenHelper.getPrimary(instrumentation) - // TODO(b/162923992) Use of multiple segments of flicker spec for testing - return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, - supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90)) { - configuration -> - withTestName { - buildTestTag("exitSplitScreenFromBottom", configuration) - } - repeat { configuration.repetitions } - setup { - eachRun { - device.wakeUpAndGoToHomeScreen() - device.openQuickStepAndClearRecentAppsFromOverview() - splitScreenApp.launchViaIntent(wmHelper) - device.launchSplitScreen() - device.waitForIdle() - this.setRotation(configuration.endRotation) - } - } - teardown { - eachRun { - if (device.isInSplitScreen()) { - device.exitSplitScreen() - } - splitScreenApp.exit() - } - } - transitions { - device.exitSplitScreenFromBottom() - } - assertions { - windowManagerTrace { - all("isNotEmpty") { isNotEmpty() } - } - } - } - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenTest.kt deleted file mode 100644 index 701b0d05e65c..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenTest.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2020 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.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.util.Rational -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry -import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.layerBecomesInvisible -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.dsl.runWithFlicker -import com.android.server.wm.flicker.helpers.exitSplitScreen -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview -import com.android.server.wm.flicker.helpers.resizeSplitScreen -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.wm.shell.flicker.dockedStackDividerIsInvisible -import com.android.wm.shell.flicker.helpers.SplitScreenHelper.Companion.TEST_REPETITIONS -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test exit SplitScreen mode. - * To run this test: `atest WMShellFlickerTests:ExitLegacySplitScreenTest` - */ -@Presubmit -@RequiresDevice -@RunWith(Parameterized::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class ExitLegacySplitScreenTest( - rotationName: String, - rotation: Int -) : SplitScreenTestBase(rotationName, rotation) { - private val splitScreenSetup: FlickerBuilder - get() = FlickerBuilder(instrumentation).apply { - val testLaunchActivity = "launch_splitScreen_test_activity" - withTestName { - testLaunchActivity - } - setup { - eachRun { - uiDevice.wakeUpAndGoToHomeScreen() - uiDevice.openQuickStepAndClearRecentAppsFromOverview() - secondaryApp.launchViaIntent() - splitScreenApp.launchViaIntent() - uiDevice.launchSplitScreen() - } - } - teardown { - eachRun { - splitScreenApp.exit() - secondaryApp.exit() - } - } - assertions { - windowManagerTrace { - visibleWindowsShownMoreThanOneConsecutiveEntry() - } - layersTrace { - visibleLayersShownMoreThanOneConsecutiveEntry( - listOf(LAUNCHER_PACKAGE_NAME)) - } - } - } - - @Test - fun testEnterSplitScreen_exitPrimarySplitScreenMode() { - val testTag = "testEnterSplitScreen_exitPrimarySplitScreenMode" - runWithFlicker(splitScreenSetup) { - withTestName { testTag } - repeat { - TEST_REPETITIONS - } - transitions { - uiDevice.exitSplitScreen() - } - assertions { - layersTrace { - dockedStackDividerIsInvisible() - layerBecomesInvisible(splitScreenApp.defaultWindowName) - } - windowManagerTrace { - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - end("appWindowIsInvisible") { - isInvisible(splitScreenApp.defaultWindowName) - } - } - } - } - } - - @Test - @FlakyTest(bugId = 172811376) - fun testEnterSplitScreen_exitPrimary_showSecondaryAppFullScreen() { - val testTag = "testEnterSplitScreen_exitPrimary_showSecondaryAppFullScreen" - runWithFlicker(splitScreenSetup) { - withTestName { testTag } - repeat { - TEST_REPETITIONS - } - transitions { - splitScreenApp.reopenAppFromOverview() - uiDevice.resizeSplitScreen(startRatio) - } - assertions { - layersTrace { - dockedStackDividerIsInvisible() - } - windowManagerTrace { - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - end("appWindowIsVisible") { - isVisible(splitScreenApp.defaultWindowName) - } - } - } - } - } - - companion object { - private val startRatio = Rational(1, 3) - - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<Array<Any>> { - val supportedRotations = intArrayOf(Surface.ROTATION_0) - return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt new file mode 100644 index 000000000000..ff3a979717f2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2021 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.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesInVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.layerBecomesInvisible +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.wm.shell.flicker.dockedStackDividerIsInvisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test dock activity to primary split screen, and open secondary to side, exit primary split + * and test secondary activity become full screen. + * To run this test: `atest WMShellFlickerTests:ExitPrimarySplitScreenShowSecondaryFullscreen` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExitPrimarySplitScreenShowSecondaryFullscreen( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testExitPrimarySplitScreenShowSecondaryFullscreen", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + device.launchSplitScreen() + secondaryApp.reopenAppFromOverview() + // TODO(b/175687842) Can not find Split screen divider, use exit() instead + splitScreenApp.exit() + } + assertions { + layersTrace { + dockedStackDividerIsInvisible(bugId = 175687842) + layerBecomesInvisible(splitScreenApp.defaultWindowName) + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, + secondaryApp.defaultWindowName), + bugId = 178447631 + ) + } + windowManagerTrace { + appWindowBecomesInVisible(splitScreenApp.defaultWindowName) + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, + secondaryApp.defaultWindowName), + bugId = 178447631 + ) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, defaultTransitionSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0) // bugId = 179116910 + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncherTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt index 4dcbdfff8cd5..03b6edf0ff2a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncherTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt @@ -52,13 +52,13 @@ import org.junit.runners.Parameterized /** * Test open app to split screen. - * To run this test: `atest WMShellFlickerTests:LegacySplitScreenToLauncherTest` + * To run this test: `atest WMShellFlickerTests:LegacySplitScreenToLauncher` */ @Presubmit @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class LegacySplitScreenToLauncherTest( +class LegacySplitScreenToLauncher( testSpec: FlickerTestRunnerFactory.TestSpec ) : FlickerTestRunner(testSpec) { companion object { @@ -67,68 +67,68 @@ class LegacySplitScreenToLauncherTest( fun getParams(): Collection<Array<Any>> { val instrumentation = InstrumentationRegistry.getInstrumentation() val launcherPackageName = LauncherStrategyFactory.getInstance(instrumentation) - .launcherStrategy.supportedLauncherPackage + .launcherStrategy.supportedLauncherPackage val testApp = SimpleAppHelper(instrumentation) // b/161435597 causes the test not to work on 90 degrees return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, supportedRotations = listOf(Surface.ROTATION_0)) { configuration -> - withTestName { - buildTestTag("splitScreenToLauncher", configuration) + withTestName { + buildTestTag("splitScreenToLauncher", configuration) + } + repeat { configuration.repetitions } + setup { + test { + device.wakeUpAndGoToHomeScreen() + device.openQuickStepAndClearRecentAppsFromOverview() } - repeat { configuration.repetitions } - setup { - test { - device.wakeUpAndGoToHomeScreen() - device.openQuickStepAndClearRecentAppsFromOverview() - } - eachRun { - testApp.launchViaIntent(wmHelper) - this.setRotation(configuration.endRotation) - device.launchSplitScreen() - device.waitForIdle() - } + eachRun { + testApp.launchViaIntent(wmHelper) + this.setRotation(configuration.endRotation) + device.launchSplitScreen() + device.waitForIdle() } - teardown { - eachRun { - testApp.exit() - } - test { - if (device.isInSplitScreen()) { - device.exitSplitScreen() - } + } + teardown { + eachRun { + testApp.exit() + } + test { + if (device.isInSplitScreen()) { + device.exitSplitScreen() } } - transitions { - device.exitSplitScreen() + } + transitions { + device.exitSplitScreen() + } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + visibleWindowsShownMoreThanOneConsecutiveEntry() } - assertions { - windowManagerTrace { - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - visibleWindowsShownMoreThanOneConsecutiveEntry() - } - layersTrace { - navBarLayerIsAlwaysVisible() - statusBarLayerIsAlwaysVisible() - noUncoveredRegions(configuration.endRotation) - navBarLayerRotatesAndScales(configuration.endRotation) - statusBarLayerRotatesScales(configuration.endRotation) - visibleLayersShownMoreThanOneConsecutiveEntry( - listOf(launcherPackageName)) + layersTrace { + navBarLayerIsAlwaysVisible() + statusBarLayerIsAlwaysVisible() + noUncoveredRegions(configuration.endRotation) + navBarLayerRotatesAndScales(configuration.endRotation) + statusBarLayerRotatesScales(configuration.endRotation) + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(launcherPackageName)) - // b/161435597 causes the test not to work on 90 degrees - dockedStackDividerBecomesInvisible() + // b/161435597 causes the test not to work on 90 degrees + dockedStackDividerBecomesInvisible() - layerBecomesInvisible(testApp.getPackage()) - } + layerBecomesInvisible(testApp.getPackage()) + } - eventLog { - focusDoesNotChange(bugId = 151179149) - } + eventLog { + focusDoesNotChange(bugId = 151179149) } } + } } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt new file mode 100644 index 000000000000..328ff88cd41b --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2020 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/LICENSE2.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.wm.shell.flicker.legacysplitscreen + +import android.app.Instrumentation +import android.os.Bundle +import android.support.test.launcherhelper.LauncherStrategyFactory +import android.view.Surface +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.startRotation +import com.android.wm.shell.flicker.helpers.SplitScreenHelper + +abstract class LegacySplitScreenTransition( + protected val instrumentation: Instrumentation +) { + internal val splitScreenApp = SplitScreenHelper.getPrimary(instrumentation) + internal val secondaryApp = SplitScreenHelper.getSecondary(instrumentation) + internal val nonResizeableApp = SplitScreenHelper.getNonResizeable(instrumentation) + internal val LAUNCHER_PACKAGE_NAME = LauncherStrategyFactory.getInstance(instrumentation) + .launcherStrategy.supportedLauncherPackage + internal val LIVE_WALLPAPER_PACKAGE_NAME = + "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2" + internal val LETTERBOX_NAME = "Letterbox" + internal val TOAST_NAME = "Toast" + internal val SPLASH_SCREEN_NAME = "Splash Screen" + + internal open val defaultTransitionSetup: FlickerBuilder.(Bundle) -> Unit + get() = { configuration -> + setup { + eachRun { + device.wakeUpAndGoToHomeScreen() + device.openQuickStepAndClearRecentAppsFromOverview() + secondaryApp.launchViaIntent(wmHelper) + splitScreenApp.launchViaIntent(wmHelper) + this.setRotation(configuration.startRotation) + } + } + teardown { + eachRun { + splitScreenApp.exit() + secondaryApp.exit() + this.setRotation(Surface.ROTATION_0) + } + } + } + + internal open val cleanSetup: FlickerBuilder.(Bundle) -> Unit + get() = { configuration -> + setup { + eachRun { + device.wakeUpAndGoToHomeScreen() + device.openQuickStepAndClearRecentAppsFromOverview() + this.setRotation(configuration.startRotation) + } + } + teardown { + eachRun { + nonResizeableApp.exit() + this.setRotation(Surface.ROTATION_0) + } + } + } + + internal open val customRotateSetup: FlickerBuilder.(Bundle) -> Unit + get() = { configuration -> + setup { + eachRun { + device.wakeUpAndGoToHomeScreen() + device.openQuickStepAndClearRecentAppsFromOverview() + secondaryApp.launchViaIntent(wmHelper) + splitScreenApp.launchViaIntent(wmHelper) + } + } + teardown { + eachRun { + splitScreenApp.exit() + secondaryApp.exit() + this.setRotation(Surface.ROTATION_0) + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableDismissInLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableDismissInLegacySplitScreen.kt new file mode 100644 index 000000000000..f2a7cda3b42d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableDismissInLegacySplitScreen.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2021 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.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesInVisible +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.layerBecomesInvisible +import com.android.server.wm.flicker.layerBecomesVisible +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test launch non resizable activity in split screen mode will trigger exit split screen mode + * (Non resizable activity launch via recent overview) + * To run this test: `atest WMShellFlickerTests:NonResizableDismissInLegacySplitScreen` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class NonResizableDismissInLegacySplitScreen( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testNonResizableDismissInLegacySplitScreen", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + nonResizeableApp.launchViaIntent(wmHelper) + splitScreenApp.launchViaIntent(wmHelper) + device.launchSplitScreen() + nonResizeableApp.reopenAppFromOverview() + wmHelper.waitForAppTransitionIdle() + } + assertions { + layersTrace { + layerBecomesVisible(nonResizeableApp.defaultWindowName) + layerBecomesInvisible(splitScreenApp.defaultWindowName) + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, + LETTERBOX_NAME, TOAST_NAME, + splitScreenApp.defaultWindowName, + nonResizeableApp.defaultWindowName), + bugId = 178447631 + ) + } + windowManagerTrace { + appWindowBecomesVisible(nonResizeableApp.defaultWindowName) + appWindowBecomesInVisible(splitScreenApp.defaultWindowName) + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, + LETTERBOX_NAME, TOAST_NAME, + splitScreenApp.defaultWindowName, + nonResizeableApp.defaultWindowName), + bugId = 178447631 + ) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, cleanSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0 /* bugId = 178685668 */)) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableDismissInLegacySplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableDismissInLegacySplitScreenTest.kt deleted file mode 100644 index 6fca5809b4fa..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableDismissInLegacySplitScreenTest.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2021 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.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.dsl.runWithFlicker -import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry -import com.android.wm.shell.flicker.dockedStackDividerIsInvisible -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test open app to split screen. - * To run this test: `atest WMShellFlickerTests:NonResizableDismissInLegacySplitScreenTest` - */ -@Presubmit -@RequiresDevice -@RunWith(Parameterized::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class NonResizableDismissInLegacySplitScreenTest( - rotationName: String, - rotation: Int -) : SplitScreenTestBase(rotationName, rotation) { - - @Test - fun testNonResizableDismissInLegacySplitScreenTest() { - val testTag = "testNonResizableDismissInLegacySplitScreenTest" - - runWithFlicker(transitionSetup) { - withTestName { testTag } - repeat { SplitScreenHelper.TEST_REPETITIONS } - transitions { - nonResizeableApp.launchViaIntent(wmHelper) - splitScreenApp.launchViaIntent(wmHelper) - device.launchSplitScreen() - nonResizeableApp.reopenAppFromOverview() - } - assertions { - layersTrace { - dockedStackDividerIsInvisible() - end("appsEndingBounds", enabled = false) { - val displayBounds = WindowUtils.getDisplayBounds(rotation) - this.hasVisibleRegion(nonResizeableApp.defaultWindowName, displayBounds) - } - visibleLayersShownMoreThanOneConsecutiveEntry( - listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, - nonResizeableApp.defaultWindowName, LETTER_BOX_NAME, - TOAST_NAME, LIVE_WALLPAPER_PACKAGE_NAME), - bugId = 178447631 - ) - } - windowManagerTrace { - end("nonResizeableAppWindowIsVisible") { - isVisible(nonResizeableApp.defaultWindowName) - .isInvisible(splitScreenApp.defaultWindowName) - } - } - } - } - } - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<Array<Any>> { - val supportedRotations = intArrayOf(Surface.ROTATION_0, Surface.ROTATION_90) - return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableLaunchInLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableLaunchInLegacySplitScreen.kt new file mode 100644 index 000000000000..421ecffc97d8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableLaunchInLegacySplitScreen.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 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.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesInVisible +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.layerBecomesInvisible +import com.android.server.wm.flicker.layerBecomesVisible +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test launch non resizable activity in split screen mode will trigger exit split screen mode + * (Non resizable activity launch via intent) + * To run this test: `atest WMShellFlickerTests:NonResizableLaunchInLegacySplitScreen` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class NonResizableLaunchInLegacySplitScreen( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testNonResizableLaunchInLegacySplitScreen", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + splitScreenApp.launchViaIntent(wmHelper) + device.launchSplitScreen() + nonResizeableApp.launchViaIntent(wmHelper) + wmHelper.waitForAppTransitionIdle() + } + assertions { + layersTrace { + layerBecomesVisible(nonResizeableApp.defaultWindowName) + layerBecomesInvisible(splitScreenApp.defaultWindowName) + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(DOCKED_STACK_DIVIDER, + LAUNCHER_PACKAGE_NAME, + LETTERBOX_NAME, + nonResizeableApp.defaultWindowName, + splitScreenApp.defaultWindowName), + bugId = 178447631 + ) + } + windowManagerTrace { + appWindowBecomesVisible(nonResizeableApp.defaultWindowName) + appWindowBecomesInVisible(splitScreenApp.defaultWindowName) + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(DOCKED_STACK_DIVIDER, + LAUNCHER_PACKAGE_NAME, + LETTERBOX_NAME, + nonResizeableApp.defaultWindowName, + splitScreenApp.defaultWindowName), + bugId = 178447631 + ) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, cleanSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0 /* bugId = 178685668 */)) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableLaunchInLegacySplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableLaunchInLegacySplitScreenTest.kt deleted file mode 100644 index deae41fae0ca..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableLaunchInLegacySplitScreenTest.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2021 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.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.dsl.runWithFlicker -import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry -import com.android.wm.shell.flicker.dockedStackDividerIsInvisible -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test open app to split screen. - * To run this test: `atest WMShellFlickerTests:NonResizableLaunchInLegacySplitScreenTest` - */ -@Presubmit -@RequiresDevice -@RunWith(Parameterized::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class NonResizableLaunchInLegacySplitScreenTest( - rotationName: String, - rotation: Int -) : SplitScreenTestBase(rotationName, rotation) { - - @Test - fun testNonResizableLaunchInLegacySplitScreenTest() { - val testTag = "testNonResizableLaunchInLegacySplitScreenTest" - - runWithFlicker(transitionSetup) { - withTestName { testTag } - repeat { SplitScreenHelper.TEST_REPETITIONS } - transitions { - nonResizeableApp.launchViaIntent(wmHelper) - splitScreenApp.launchViaIntent(wmHelper) - device.launchSplitScreen() - nonResizeableApp.reopenAppFromOverview() - } - assertions { - layersTrace { - dockedStackDividerIsInvisible() - end("appsEndingBounds", enabled = false) { - val displayBounds = WindowUtils.getDisplayBounds(rotation) - this.hasVisibleRegion(nonResizeableApp.defaultWindowName, displayBounds) - } - visibleLayersShownMoreThanOneConsecutiveEntry( - listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, - nonResizeableApp.defaultWindowName, LETTER_BOX_NAME, - TOAST_NAME, LIVE_WALLPAPER_PACKAGE_NAME), - bugId = 178447631 - ) - } - windowManagerTrace { - end("nonResizeableAppWindowIsVisible") { - isVisible(nonResizeableApp.defaultWindowName) - .isInvisible(splitScreenApp.defaultWindowName) - } - } - } - } - } - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<Array<Any>> { - val supportedRotations = intArrayOf(Surface.ROTATION_0, Surface.ROTATION_90) - return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt index 6200a69b795e..7edef9314941 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreenTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt @@ -16,94 +16,87 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.os.Bundle import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory import com.android.server.wm.flicker.appWindowBecomesVisible -import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible -import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry -import com.android.server.wm.flicker.layerBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.focusChanges +import com.android.server.wm.flicker.helpers.buildTestTag import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.dsl.runWithFlicker -import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.layerBecomesVisible import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import com.android.wm.shell.flicker.appPairsDividerBecomesVisible import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder -import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** * Test open app to split screen. - * To run this test: `atest WMShellFlickerTests:OpenAppToLegacySplitScreenTest` + * To run this test: `atest WMShellFlickerTests:OpenAppToLegacySplitScreen` */ @Presubmit @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class OpenAppToLegacySplitScreenTest( - rotationName: String, - rotation: Int -) : SplitScreenTestBase(rotationName, rotation) { - @Test - fun OpenAppToLegacySplitScreenTest() { - val testTag = "OpenAppToLegacySplitScreenTest" - val helper = WindowManagerStateHelper() - runWithFlicker(transitionSetup) { - withTestName { testTag } - repeat { SplitScreenHelper.TEST_REPETITIONS } - setup { - eachRun { - splitScreenApp.launchViaIntent(wmHelper) - device.pressHome() - this.setRotation(rotation) +class OpenAppToLegacySplitScreen( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val wmHelper = WindowManagerStateHelper() + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testOpenAppToLegacySplitScreen", configuration) } - } - transitions { - device.launchSplitScreen() - helper.waitForAppTransitionIdle() - } - assertions { - windowManagerTrace { - visibleWindowsShownMoreThanOneConsecutiveEntry( - listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, - LETTER_BOX_NAME) - ) - appWindowBecomesVisible(splitScreenApp.getPackage()) + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + device.launchSplitScreen() + wmHelper.waitForAppTransitionIdle() } + assertions { + windowManagerTrace { + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName), + bugId = 178447631) + appWindowBecomesVisible(splitScreenApp.getPackage()) + } - layersTrace { - navBarLayerIsAlwaysVisible() - noUncoveredRegions(rotation, enabled = false) - statusBarLayerIsAlwaysVisible() - visibleLayersShownMoreThanOneConsecutiveEntry( - listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, - LETTER_BOX_NAME)) - appPairsDividerBecomesVisible() - layerBecomesVisible(splitScreenApp.getPackage()) - } + layersTrace { + noUncoveredRegions(configuration.startRotation, enabled = false) + statusBarLayerIsAlwaysVisible() + appPairsDividerBecomesVisible() + layerBecomesVisible(splitScreenApp.getPackage()) + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName), + bugId = 178447631) + } - eventLog { - focusChanges(splitScreenApp.`package`, + eventLog { + focusChanges(splitScreenApp.`package`, "recents_animation_input_consumer", "NexusLauncherActivity", bugId = 151179149) + } } } - } - } - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<Array<Any>> { - // TODO(b/161435597) causes the test not to work on 90 degrees - val supportedRotations = intArrayOf(Surface.ROTATION_0) - return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, defaultTransitionSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0) // bugId = 179116910 + ) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt index 95c1c16385c4..54a37d71868d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreenTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt @@ -58,7 +58,7 @@ import org.junit.runners.Parameterized /** * Test split screen resizing window transitions. - * To run this test: `atest WMShellFlickerTests:ResizeLegacySplitScreenTest` + * To run this test: `atest WMShellFlickerTests:ResizeLegacySplitScreen` * * Currently it runs only in 0 degrees because of b/156100803 */ @@ -67,7 +67,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @FlakyTest(bugId = 159096424) -class ResizeLegacySplitScreenTest( +class ResizeLegacySplitScreen( testSpec: FlickerTestRunnerFactory.TestSpec ) : FlickerTestRunner(testSpec) { companion object { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt new file mode 100644 index 000000000000..214269e13203 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2020 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.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test dock activity to primary split screen and rotate + * To run this test: `atest WMShellFlickerTests:RotateOneLaunchedAppAndEnterSplitScreen` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class RotateOneLaunchedAppAndEnterSplitScreen( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testRotateOneLaunchedAppAndEnterSplitScreen", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + device.launchSplitScreen() + this.setRotation(configuration.startRotation) + } + assertions { + layersTrace { + dockedStackDividerIsVisible(bugId = 175687842) + dockedStackPrimaryBoundsIsVisible( + configuration.startRotation, + splitScreenApp.defaultWindowName, bugId = 175687842) + navBarLayerRotatesAndScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + statusBarLayerRotatesScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + } + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + appWindowBecomesVisible(splitScreenApp.defaultWindowName) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, customRotateSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0 /* bugId = 178685668 */)) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt new file mode 100644 index 000000000000..4290c923b38d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2020 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.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Rotate + * To run this test: `atest WMShellFlickerTests:RotateOneLaunchedAppInSplitScreenMode` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class RotateOneLaunchedAppInSplitScreenMode( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testRotateOneLaunchedAppInSplitScreenMode", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + this.setRotation(configuration.startRotation) + device.launchSplitScreen() + } + assertions { + layersTrace { + dockedStackDividerIsVisible(bugId = 175687842) + dockedStackPrimaryBoundsIsVisible( + configuration.startRotation, + splitScreenApp.defaultWindowName, bugId = 175687842) + navBarLayerRotatesAndScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + statusBarLayerRotatesScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + } + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + appWindowBecomesVisible(splitScreenApp.defaultWindowName) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, customRotateSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0 /* bugId = 178685668 */)) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppTest.kt deleted file mode 100644 index 07571c3218a8..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppTest.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2020 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.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.dsl.runWithFlicker -import com.android.server.wm.flicker.helpers.exitSplitScreen -import com.android.server.wm.flicker.helpers.isInSplitScreen -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.dockedStackDividerIsVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test open app to split screen. - * To run this test: `atest WMShellFlickerTests:RotateOneLaunchedAppTest` - */ -@Presubmit -@RequiresDevice -@RunWith(Parameterized::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class RotateOneLaunchedAppTest( - rotationName: String, - rotation: Int -) : SplitScreenTestBase(rotationName, rotation) { - private val splitScreenRotationSetup: FlickerBuilder - get() = FlickerBuilder(instrumentation).apply { - val testSetupRotation = "testSetupRotation" - withTestName { - testSetupRotation - } - setup { - test { - uiDevice.wakeUpAndGoToHomeScreen() - uiDevice.openQuickStepAndClearRecentAppsFromOverview() - } - } - teardown { - eachRun { - if (uiDevice.isInSplitScreen()) { - uiDevice.exitSplitScreen() - } - setRotation(Surface.ROTATION_0) - splitScreenApp.exit() - secondaryApp.exit() - } - } - } - - @Test - fun testRotateInSplitScreenMode() { - val testTag = "testEnterSplitScreen_launchToSide" - runWithFlicker(splitScreenRotationSetup) { - withTestName { testTag } - repeat { - SplitScreenHelper.TEST_REPETITIONS - } - transitions { - splitScreenApp.launchViaIntent() - uiDevice.launchSplitScreen() - setRotation(rotation) - } - assertions { - layersTrace { - navBarLayerRotatesAndScales(Surface.ROTATION_0, rotation, 169271943) - statusBarLayerRotatesScales(Surface.ROTATION_0, rotation, 169271943) - dockedStackDividerIsVisible() - dockedStackPrimaryBoundsIsVisible( - rotation, splitScreenApp.defaultWindowName, 169271943) - } - windowManagerTrace { - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - end("appWindowIsVisible") { - isVisible(splitScreenApp.defaultWindowName) - } - } - } - } - } - - @Test - fun testRotateAndEnterSplitScreenMode() { - val testTag = "testRotateAndEnterSplitScreenMode" - runWithFlicker(splitScreenRotationSetup) { - withTestName { testTag } - repeat { - SplitScreenHelper.TEST_REPETITIONS - } - transitions { - splitScreenApp.launchViaIntent() - setRotation(rotation) - uiDevice.launchSplitScreen() - } - assertions { - layersTrace { - navBarLayerRotatesAndScales(Surface.ROTATION_0, rotation, 169271943) - statusBarLayerRotatesScales(Surface.ROTATION_0, rotation, 169271943) - dockedStackDividerIsVisible() - dockedStackPrimaryBoundsIsVisible( - rotation, splitScreenApp.defaultWindowName, 169271943) - } - windowManagerTrace { - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - end("appWindowIsVisible") { - isVisible(splitScreenApp.defaultWindowName) - } - } - } - } - } - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<Array<Any>> { - val supportedRotations = intArrayOf(Surface.ROTATION_90, Surface.ROTATION_270) - return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt new file mode 100644 index 000000000000..4095b9a2e61e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2020 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.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open app to split screen. + * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppAndEnterSplitScreen` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class RotateTwoLaunchedAppAndEnterSplitScreen( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testRotateTwoLaunchedAppAndEnterSplitScreen", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + this.setRotation(configuration.startRotation) + device.launchSplitScreen() + secondaryApp.reopenAppFromOverview() + } + assertions { + layersTrace { + dockedStackDividerIsVisible(bugId = 175687842) + dockedStackPrimaryBoundsIsVisible( + configuration.startRotation, + splitScreenApp.defaultWindowName, 175687842) + dockedStackSecondaryBoundsIsVisible( + configuration.startRotation, + secondaryApp.defaultWindowName, bugId = 175687842) + navBarLayerRotatesAndScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + statusBarLayerRotatesScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + } + windowManagerTrace { + appWindowBecomesVisible(secondaryApp.defaultWindowName) + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, customRotateSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0 /* bugId = 178685668 */)) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt new file mode 100644 index 000000000000..aebf6067615e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2020 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.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open app to split screen. + * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppInSplitScreenMode` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class RotateTwoLaunchedAppInSplitScreenMode( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testRotateTwoLaunchedAppInSplitScreenMode", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + setup { + eachRun { + device.launchSplitScreen() + splitScreenApp.reopenAppFromOverview() + this.setRotation(configuration.startRotation) + } + } + transitions { + this.setRotation(configuration.startRotation) + } + assertions { + layersTrace { + dockedStackDividerIsVisible(bugId = 175687842) + dockedStackPrimaryBoundsIsVisible( + configuration.startRotation, + splitScreenApp.defaultWindowName, bugId = 175687842) + dockedStackSecondaryBoundsIsVisible( + configuration.startRotation, + secondaryApp.defaultWindowName, bugId = 175687842) + navBarLayerRotatesAndScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + statusBarLayerRotatesScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + } + windowManagerTrace { + appWindowBecomesVisible(secondaryApp.defaultWindowName) + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, customRotateSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0 /* bugId = 178685668 */)) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppTest.kt deleted file mode 100644 index d8014d37dfad..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppTest.kt +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (C) 2020 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.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.dsl.runWithFlicker -import com.android.server.wm.flicker.helpers.exitSplitScreen -import com.android.server.wm.flicker.helpers.isInSplitScreen -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.dockedStackDividerIsVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible -import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test open app to split screen. - * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppTest` - */ -@Presubmit -@RequiresDevice -@RunWith(Parameterized::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class RotateTwoLaunchedAppTest( - rotationName: String, - rotation: Int -) : SplitScreenTestBase(rotationName, rotation) { - private val splitScreenRotationSetup: FlickerBuilder - get() = FlickerBuilder(instrumentation).apply { - val testSetupRotation = "testSetupRotation" - withTestName { - testSetupRotation - } - setup { - test { - uiDevice.wakeUpAndGoToHomeScreen() - uiDevice.openQuickStepAndClearRecentAppsFromOverview() - } - } - teardown { - eachRun { - if (uiDevice.isInSplitScreen()) { - uiDevice.exitSplitScreen() - } - setRotation(Surface.ROTATION_0) - splitScreenApp.exit() - secondaryApp.exit() - } - } - } - - @Test - fun testRotateInSplitScreenMode() { - val testTag = "testRotateInSplitScreenMode" - runWithFlicker(splitScreenRotationSetup) { - withTestName { testTag } - repeat { - SplitScreenHelper.TEST_REPETITIONS - } - transitions { - secondaryApp.launchViaIntent() - splitScreenApp.launchViaIntent() - uiDevice.launchSplitScreen() - splitScreenApp.reopenAppFromOverview() - setRotation(rotation) - } - assertions { - layersTrace { - navBarLayerRotatesAndScales(Surface.ROTATION_0, rotation, 169271943) - statusBarLayerRotatesScales(Surface.ROTATION_0, rotation, 169271943) - dockedStackDividerIsVisible() - dockedStackPrimaryBoundsIsVisible( - rotation, splitScreenApp.defaultWindowName, 169271943) - dockedStackSecondaryBoundsIsVisible( - rotation, secondaryApp.defaultWindowName, 169271943) - } - windowManagerTrace { - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - end("appWindowIsVisible") { - isVisible(splitScreenApp.defaultWindowName) - .isVisible(secondaryApp.defaultWindowName) - } - } - } - } - } - - @FlakyTest(bugId = 173875043) - @Test - fun testRotateAndEnterSplitScreenMode() { - val testTag = "testRotateAndEnterSplitScreenMode" - runWithFlicker(splitScreenRotationSetup) { - withTestName { testTag } - repeat { - SplitScreenHelper.TEST_REPETITIONS - } - transitions { - secondaryApp.launchViaIntent() - splitScreenApp.launchViaIntent() - setRotation(rotation) - uiDevice.launchSplitScreen() - splitScreenApp.reopenAppFromOverview() - } - assertions { - layersTrace { - navBarLayerRotatesAndScales(Surface.ROTATION_0, rotation, 169271943) - statusBarLayerRotatesScales(Surface.ROTATION_0, rotation, 169271943) - dockedStackDividerIsVisible() - dockedStackPrimaryBoundsIsVisible( - rotation, splitScreenApp.defaultWindowName, 169271943) - dockedStackSecondaryBoundsIsVisible( - rotation, secondaryApp.defaultWindowName, 169271943) - } - windowManagerTrace { - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - end("appWindowIsVisible") { - isVisible(splitScreenApp.defaultWindowName) - .isVisible(secondaryApp.defaultWindowName) - } - } - } - } - } - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<Array<Any>> { - val supportedRotations = intArrayOf(Surface.ROTATION_90, Surface.ROTATION_270) - return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/SplitScreenTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/SplitScreenTestBase.kt deleted file mode 100644 index 01db4ed6253e..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/SplitScreenTestBase.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2020 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.wm.shell.flicker.legacysplitscreen - -import android.support.test.launcherhelper.LauncherStrategyFactory -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.exitSplitScreen -import com.android.server.wm.flicker.helpers.isInSplitScreen -import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.NonRotationTestBase -import com.android.wm.shell.flicker.helpers.SplitScreenHelper - -abstract class SplitScreenTestBase( - rotationName: String, - rotation: Int -) : NonRotationTestBase(rotationName, rotation) { - protected val splitScreenApp = SplitScreenHelper.getPrimary(instrumentation) - protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation) - protected val nonResizeableApp = SplitScreenHelper.getNonResizeable(instrumentation) - protected val LAUNCHER_PACKAGE_NAME = LauncherStrategyFactory.getInstance(instrumentation) - .launcherStrategy.supportedLauncherPackage - protected val LIVE_WALLPAPER_PACKAGE_NAME = - "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2" - protected val LETTER_BOX_NAME = "Letterbox" - protected val TOAST_NAME = "Toast" - - protected val transitionSetup: FlickerBuilder - get() = FlickerBuilder(instrumentation).apply { - setup { - eachRun { - uiDevice.wakeUpAndGoToHomeScreen() - uiDevice.openQuickStepAndClearRecentAppsFromOverview() - } - } - teardown { - eachRun { - if (uiDevice.isInSplitScreen()) { - uiDevice.exitSplitScreen() - } - splitScreenApp.exit() - nonResizeableApp.exit() - } - } - assertions { - layersTrace { - navBarLayerIsAlwaysVisible() - statusBarLayerIsAlwaysVisible() - } - windowManagerTrace { - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - } - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AppTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AppTestBase.kt index 2015f4941cea..bc42d5ed04ce 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AppTestBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AppTestBase.kt @@ -16,11 +16,6 @@ package com.android.wm.shell.flicker.pip -import android.app.ActivityTaskManager -import android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT -import android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS -import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD -import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED import android.os.SystemClock import com.android.wm.shell.flicker.NonRotationTestBase @@ -29,14 +24,6 @@ abstract class AppTestBase( rotation: Int ) : NonRotationTestBase(rotationName, rotation) { companion object { - fun removeAllTasksButHome() { - val ALL_ACTIVITY_TYPE_BUT_HOME = intArrayOf( - ACTIVITY_TYPE_STANDARD, ACTIVITY_TYPE_ASSISTANT, ACTIVITY_TYPE_RECENTS, - ACTIVITY_TYPE_UNDEFINED) - val atm = ActivityTaskManager.getService() - atm.removeRootTasksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME) - } - fun waitForAnimationComplete() { // TODO: UiDevice doesn't have reliable way to wait for the completion of animation. // Consider to introduce WindowManagerStateHelper to access Activity state. diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt index 5a3d18d9feef..d56ed02972fb 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt @@ -16,21 +16,19 @@ package com.android.wm.shell.flicker.pip -import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.dsl.runFlicker +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen import com.android.wm.shell.flicker.helpers.FixedAppHelper -import com.android.wm.shell.flicker.helpers.PipAppHelper import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP import org.junit.FixMethodOrder -import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized @@ -39,82 +37,66 @@ import org.junit.runners.Parameterized * Test Pip launch and exit. * To run this test: `atest WMShellFlickerTests:EnterExitPipTest` */ -@Presubmit @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) class EnterExitPipTest( - rotationName: String, - rotation: Int -) : AppTestBase(rotationName, rotation) { - private val pipApp = PipAppHelper(instrumentation) - private val testApp = FixedAppHelper(instrumentation) - - @Test - fun testDisplayMetricsPinUnpin() { - runFlicker(instrumentation) { - withTestName { "testDisplayMetricsPinUnpin" } - setup { - test { - removeAllTasksButHome() - device.wakeUpAndGoToHomeScreen() - pipApp.launchViaIntent(stringExtras = mapOf(EXTRA_ENTER_PIP to "true")) - testApp.launchViaIntent() - waitForAnimationComplete() - } - } - transitions { - // This will bring PipApp to fullscreen - pipApp.launchViaIntent() - waitForAnimationComplete() - } - teardown { - test { - removeAllTasksButHome() - } - } - assertions { - val displayBounds = WindowUtils.getDisplayBounds(rotation) - windowManagerTrace { - all("pipApp must remain inside visible bounds") { - coversAtMostRegion(pipApp.defaultWindowName, displayBounds) - } - all("Initially shows both app windows then pipApp hides testApp") { - showsAppWindow(testApp.defaultWindowName) - .showsAppWindowOnTop(pipApp.defaultWindowName) - .then() - .hidesAppWindow(testApp.defaultWindowName) + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : PipTransitionBase(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<Array<Any>> { + val testApp = FixedAppHelper(instrumentation) + val testSpec = getTransition(eachRun = true) { configuration -> + setup { + eachRun { + testApp.launchViaIntent(wmHelper) } - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() } - layersTrace { - all("Initially shows both app layers then pipApp hides testApp") { - showsLayer(testApp.defaultWindowName) - .showsLayer(pipApp.defaultWindowName) - .then() - .hidesLayer(testApp.defaultWindowName) - } - start("testApp covers the fullscreen, pipApp remains inside display") { - hasVisibleRegion(testApp.defaultWindowName, displayBounds) - coversAtMostRegion(displayBounds, pipApp.defaultWindowName) - } - end("pipApp covers the fullscreen") { - hasVisibleRegion(pipApp.defaultWindowName, displayBounds) + transitions { + // This will bring PipApp to fullscreen + pipApp.launchViaIntent(wmHelper) + } + assertions { + val displayBounds = WindowUtils.getDisplayBounds(configuration.startRotation) + presubmit { + windowManagerTrace { + all("pipApp must remain inside visible bounds") { + coversAtMostRegion(pipApp.defaultWindowName, displayBounds) + } + all("Initially shows both app windows then pipApp hides testApp") { + showsAppWindow(testApp.defaultWindowName) + .showsAppWindowOnTop(pipApp.defaultWindowName) + .then() + .hidesAppWindow(testApp.defaultWindowName) + } + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + layersTrace { + all("Initially shows both app layers then pipApp hides testApp") { + showsLayer(testApp.defaultWindowName) + .showsLayer(pipApp.defaultWindowName) + .then() + .hidesLayer(testApp.defaultWindowName) + } + start("testApp covers the fullscreen, pipApp remains inside display") { + hasVisibleRegion(testApp.defaultWindowName, displayBounds) + coversAtMostRegion(displayBounds, pipApp.defaultWindowName) + } + end("pipApp covers the fullscreen") { + hasVisibleRegion(pipApp.defaultWindowName, displayBounds) + } + navBarLayerIsAlwaysVisible() + statusBarLayerIsAlwaysVisible() + } } - navBarLayerIsAlwaysVisible() - statusBarLayerIsAlwaysVisible() } } - } - } - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<Array<Any>> { - val supportedRotations = intArrayOf(Surface.ROTATION_0) - return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + testSpec, supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 5) } } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt index af62eb9ae40d..ff31ba7d2c01 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt @@ -17,17 +17,10 @@ package com.android.wm.shell.flicker.pip import android.view.Surface -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerTestRunner import com.android.server.wm.flicker.FlickerTestRunnerFactory -import com.android.server.wm.flicker.helpers.buildTestTag -import com.android.server.wm.flicker.helpers.closePipWindow -import com.android.server.wm.flicker.helpers.expandPipWindow -import com.android.server.wm.flicker.helpers.hasPipWindow -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible @@ -35,9 +28,7 @@ import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible import com.android.server.wm.flicker.noUncoveredRegions -import com.android.server.wm.flicker.repetitions import com.android.server.wm.flicker.startRotation -import com.android.wm.shell.flicker.helpers.PipAppHelper import org.junit.FixMethodOrder import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -50,80 +41,57 @@ import org.junit.runners.Parameterized @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@FlakyTest(bugId = 152738416) class EnterPipTest( testSpec: FlickerTestRunnerFactory.TestSpec ) : FlickerTestRunner(testSpec) { - companion object { + companion object : PipTransitionBase(InstrumentationRegistry.getInstrumentation()) { @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): List<Array<Any>> { - val instrumentation = InstrumentationRegistry.getInstrumentation() - val testApp = PipAppHelper(instrumentation) - return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, - supportedRotations = listOf(Surface.ROTATION_0)) { configuration -> - withTestName { buildTestTag("enterPip", testApp, configuration) } - repeat { configuration.repetitions } - setup { - test { - device.wakeUpAndGoToHomeScreen() - } - eachRun { - device.pressHome() - testApp.launchViaIntent(wmHelper) - this.setRotation(configuration.startRotation) - } - } - teardown { - eachRun { - if (device.hasPipWindow()) { - device.closePipWindow() - } - testApp.exit() - this.setRotation(Surface.ROTATION_0) - } - test { - if (device.hasPipWindow()) { - device.closePipWindow() - } - } - } - transitions { - testApp.clickEnterPipButton() - device.expandPipWindow() - } - assertions { + val testSpec = getTransition(eachRun = true, + stringExtras = emptyMap()) { configuration -> + transitions { + pipApp.clickEnterPipButton() + pipApp.expandPipWindow(wmHelper) + } + assertions { + presubmit { windowManagerTrace { navBarWindowIsAlwaysVisible() statusBarWindowIsAlwaysVisible() all("pipWindowBecomesVisible") { - this.showsAppWindow(testApp.`package`) - .then() - .showsAppWindow(PIP_WINDOW_TITLE) + this.showsAppWindow(pipApp.defaultWindowName) } } layersTrace { - navBarLayerIsAlwaysVisible(bugId = 140855415) statusBarLayerIsAlwaysVisible() - noUncoveredRegions(configuration.startRotation, Surface.ROTATION_0, - enabled = false) - navBarLayerRotatesAndScales(configuration.startRotation, - Surface.ROTATION_0, bugId = 140855415) statusBarLayerRotatesScales(configuration.startRotation, Surface.ROTATION_0) } layersTrace { all("pipLayerBecomesVisible") { - this.showsLayer(testApp.launcherName) - .then() - .showsLayer(PIP_WINDOW_TITLE) + this.showsLayer(pipApp.launcherName) } } } + + flaky { + layersTrace { + navBarLayerIsAlwaysVisible(bugId = 140855415) + noUncoveredRegions(configuration.startRotation, Surface.ROTATION_0) + navBarLayerRotatesAndScales(configuration.startRotation, + Surface.ROTATION_0, bugId = 140855415) + } + } } + } + + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + testSpec, supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 5) } } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt new file mode 100644 index 000000000000..eaaa2f6390be --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2021 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.wm.shell.flicker.pip + +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.pip.PipTransitionBase.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE +import com.android.wm.shell.flicker.pip.PipTransitionBase.BroadcastActionTrigger.Companion.ORIENTATION_PORTRAIT +import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP +import com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip with orientation changes. + * To run this test: `atest WMShellFlickerTests:PipOrientationTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EnterPipToOtherOrientationTest( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : PipTransitionBase(InstrumentationRegistry.getInstrumentation()) { + private val testApp = FixedAppHelper(instrumentation) + + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 5) { configuration -> + setupAndTeardown(this, configuration) + + setup { + eachRun { + // Launch a portrait only app on the fullscreen stack + testApp.launchViaIntent(wmHelper, stringExtras = mapOf( + EXTRA_FIXED_ORIENTATION to ORIENTATION_PORTRAIT.toString())) + // Launch the PiP activity fixed as landscape + pipApp.launchViaIntent(wmHelper, stringExtras = mapOf( + EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())) + } + } + teardown { + eachRun { + pipApp.exit() + testApp.exit() + } + } + transitions { + // Enter PiP, and assert that the PiP is within bounds now that the device is back + // in portrait + broadcastActionTrigger.doAction(ACTION_ENTER_PIP) + wmHelper.waitPipWindowShown() + wmHelper.waitForAppTransitionIdle() + } + assertions { + val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90) + val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0) + + presubmit { + windowManagerTrace { + all("pipApp window is always on top") { + showsAppWindowOnTop(pipApp.defaultWindowName) + } + start("pipApp window hides testApp") { + isInvisible(testApp.defaultWindowName) + } + end("testApp windows is shown") { + isVisible(testApp.defaultWindowName) + } + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + + layersTrace { + start("pipApp layer hides testApp") { + hasVisibleRegion(pipApp.defaultWindowName, startingBounds) + isInvisible(testApp.defaultWindowName) + } + } + } + + flaky { + layersTrace { + end("testApp layer covers fullscreen") { + hasVisibleRegion(testApp.defaultWindowName, endingBounds) + } + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible(bugId = 140855415) + } + } + } + } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt new file mode 100644 index 000000000000..707d28d9c4c0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2021 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.wm.shell.flicker.pip + +import android.app.WindowConfiguration +import android.content.ComponentName +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerStateSubject +import com.android.server.wm.traces.common.windowmanager.WindowManagerState +import com.android.server.wm.traces.parser.toWindowName +import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.google.common.truth.Truth + +inline val WindowManagerState.pinnedWindows + get() = visibleWindows + .filter { it.windowingMode == WindowConfiguration.WINDOWING_MODE_PINNED } + +/** + * Checks if the state has any window in PIP mode + */ +fun WindowManagerState.hasPipWindow(): Boolean = pinnedWindows.isNotEmpty() + +/** + * Checks that an activity [activity] is in PIP mode + */ +fun WindowManagerState.isInPipMode(activity: ComponentName): Boolean { + val windowName = activity.toWindowName() + return pinnedWindows.any { it.title == windowName } +} + +/** + * Asserts that an activity [activity] exists and is in PIP mode + */ +fun WindowManagerStateSubject.isInPipMode( + activity: ComponentName +): WindowManagerStateSubject = apply { + val windowName = activity.toWindowName() + hasWindow(windowName) + val pinnedWindows = wmState.pinnedWindows + .map { it.title } + Truth.assertWithMessage("Window not in PIP mode") + .that(pinnedWindows) + .contains(windowName) +} + +/** + * Waits until the state has a window in PIP mode, i.e., with + * windowingMode = WindowConfiguration.WINDOWING_MODE_PINNED + */ +fun WindowManagerStateHelper.waitPipWindowShown(): Boolean = + waitFor("PIP window shown") { + val result = it.wmState.hasPipWindow() + result + } + +/** + * Waits until the state doesn't have a window in PIP mode, i.e., with + * windowingMode = WindowConfiguration.WINDOWING_MODE_PINNED + */ +fun WindowManagerStateHelper.waitPipWindowGone(): Boolean = + waitFor("PIP window gone") { + val result = !it.wmState.hasPipWindow() + result + } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt index c21b594246b9..f054e6412080 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt @@ -16,21 +16,17 @@ package com.android.wm.shell.flicker.pip -import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.dsl.runWithFlicker +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.helpers.closePipWindow -import com.android.server.wm.flicker.helpers.hasPipWindow -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.startRotation import com.android.wm.shell.flicker.IME_WINDOW_NAME import com.android.wm.shell.flicker.helpers.ImeAppHelper -import com.android.wm.shell.flicker.testapp.Components import org.junit.FixMethodOrder -import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized @@ -39,114 +35,60 @@ import org.junit.runners.Parameterized * Test Pip launch. * To run this test: `atest WMShellFlickerTests:PipKeyboardTest` */ -@Presubmit @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class PipKeyboardTest( - rotationName: String, - rotation: Int -) : PipTestBase(rotationName, rotation) { - private val keyboardApp = ImeAppHelper(instrumentation) - private val keyboardComponent = Components.ImeActivity.COMPONENT - private val helper = WindowManagerStateHelper() +class PipKeyboardTest(testSpec: FlickerTestRunnerFactory.TestSpec) : FlickerTestRunner(testSpec) { + companion object : PipTransitionBase(InstrumentationRegistry.getInstrumentation()) { + private const val TAG_IME_VISIBLE = "imeIsVisible" - private val keyboardScenario: FlickerBuilder - get() = FlickerBuilder(instrumentation).apply { - repeat { TEST_REPETITIONS } - // disable layer tracing - withLayerTracing { null } - setup { - test { - device.wakeUpAndGoToHomeScreen() - device.pressHome() - // launch our target pip app - testApp.launchViaIntent(wmHelper) - this.setRotation(rotation) - testApp.clickEnterPipButton() - // open an app with an input field and a keyboard - // UiAutomator doesn't support to launch the multiple Activities in a task. - // So use launchActivity() for the Keyboard Activity. - keyboardApp.launchViaIntent() - helper.waitForAppTransitionIdle() - helper.waitForFullScreenApp(keyboardComponent) - } - } - teardown { - test { - keyboardApp.exit() - - if (device.hasPipWindow()) { - device.closePipWindow() + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val imeApp = ImeAppHelper(instrumentation) + val testSpec = getTransition(eachRun = false) { configuration -> + setup { + test { + imeApp.launchViaIntent(wmHelper) + setRotation(configuration.startRotation) } - testApp.exit() - this.setRotation(Surface.ROTATION_0) } - } - } - - /** Ensure the pip window remains visible throughout any keyboard interactions. */ - @Test - fun pipWindow_doesNotLeaveTheScreen_onKeyboardOpenClose() { - val testTag = "pipWindow_doesNotLeaveTheScreen_onKeyboardOpenClose" - runWithFlicker(keyboardScenario) { - withTestName { testTag } - transitions { - // open the soft keyboard - keyboardApp.openIME(device, wmHelper) - helper.waitImeWindowShown() - - // then close it again - keyboardApp.closeIME(device, wmHelper) - helper.waitImeWindowGone() - } - assertions { - windowManagerTrace { - all("PiP window must remain inside visible bounds") { - val displayBounds = WindowUtils.getDisplayBounds(rotation) - coversAtMostRegion(testApp.defaultWindowName, displayBounds) + teardown { + test { + imeApp.exit() + setRotation(Surface.ROTATION_0) } } - } - } - } + transitions { + // open the soft keyboard + imeApp.openIME(wmHelper) + createTag(TAG_IME_VISIBLE) - /** Ensure the pip window does not obscure the keyboard. */ - @Test - fun pipWindow_doesNotObscure_keyboard() { - val testTag = "pipWindow_doesNotObscure_keyboard" - runWithFlicker(keyboardScenario) { - withTestName { testTag } - transitions { - // open the soft keyboard - keyboardApp.openIME(device, wmHelper) - helper.waitImeWindowShown() - } - teardown { - eachRun { - // close the keyboard - keyboardApp.closeIME(device, wmHelper) - helper.waitImeWindowGone() + // then close it again + imeApp.closeIME(wmHelper) } - } - assertions { - windowManagerTrace { - end("imeWindowAboveApp") { - isAboveWindow(IME_WINDOW_NAME, testApp.defaultWindowName) + assertions { + presubmit { + windowManagerTrace { + // Ensure the pip window remains visible throughout + // any keyboard interactions + all("pipInVisibleBounds") { + val displayBounds = WindowUtils.getDisplayBounds( + configuration.startRotation) + coversAtMostRegion(pipApp.defaultWindowName, displayBounds) + } + // Ensure that the pip window does not obscure the keyboard + tag(TAG_IME_VISIBLE) { + isAboveWindow(IME_WINDOW_NAME, pipApp.defaultWindowName) + } + } } } } - } - } - - companion object { - private const val TEST_REPETITIONS = 5 - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<Array<Any>> { - val supportedRotations = intArrayOf(Surface.ROTATION_0) - return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + testSpec, supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 5) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt index e5790962c025..f10bd7f1e45a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt @@ -32,6 +32,7 @@ import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.removeAllTasksButHome import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP import org.junit.FixMethodOrder import org.junit.Test diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipOrientationTest.kt deleted file mode 100644 index 5e0760ceeda7..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipOrientationTest.kt +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright (C) 2020 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.wm.shell.flicker.pip - -import android.content.Intent -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.dsl.runFlicker -import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.wm.shell.flicker.helpers.FixedAppHelper -import com.android.wm.shell.flicker.helpers.PipAppHelper -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible -import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP -import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION -import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP -import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_PIP_ORIENTATION -import com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION -import org.junit.Assert.assertEquals -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test Pip with orientation changes. - * To run this test: `atest WMShellFlickerTests:PipOrientationTest` - */ -@Presubmit -@RequiresDevice -@RunWith(Parameterized::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class PipOrientationTest( - rotationName: String, - rotation: Int -) : AppTestBase(rotationName, rotation) { - // Helper class to process test actions by broadcast. - private inner class BroadcastActionTrigger { - private fun createIntentWithAction(broadcastAction: String): Intent { - return Intent(broadcastAction).setFlags(Intent.FLAG_RECEIVER_FOREGROUND) - } - fun doAction(broadcastAction: String) { - instrumentation.getContext().sendBroadcast(createIntentWithAction(broadcastAction)) - } - fun requestOrientationForPip(orientation: Int) { - instrumentation.getContext() - .sendBroadcast(createIntentWithAction(ACTION_SET_REQUESTED_ORIENTATION) - .putExtra(EXTRA_PIP_ORIENTATION, orientation.toString())) - } - } - private val broadcastActionTrigger = BroadcastActionTrigger() - - // Corresponds to ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - private val ORIENTATION_LANDSCAPE = 0 - // Corresponds to ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - private val ORIENTATION_PORTRAIT = 1 - - private val testApp = FixedAppHelper(instrumentation) - private val pipApp = PipAppHelper(instrumentation) - - @Test - fun testEnterPipToOtherOrientation() { - runFlicker(instrumentation) { - withTestName { "testEnterPipToOtherOrientation" } - setup { - test { - removeAllTasksButHome() - device.wakeUpAndGoToHomeScreen() - // Launch a portrait only app on the fullscreen stack - testApp.launchViaIntent(stringExtras = mapOf( - EXTRA_FIXED_ORIENTATION to ORIENTATION_PORTRAIT.toString())) - waitForAnimationComplete() - // Launch the PiP activity fixed as landscape - pipApp.launchViaIntent(stringExtras = mapOf( - EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())) - waitForAnimationComplete() - } - } - transitions { - // Enter PiP, and assert that the PiP is within bounds now that the device is back - // in portrait - broadcastActionTrigger.doAction(ACTION_ENTER_PIP) - waitForAnimationComplete() - } - teardown { - test { - removeAllTasksButHome() - } - } - assertions { - windowManagerTrace { - all("pipApp window is always on top") { - showsAppWindowOnTop(pipApp.defaultWindowName) - } - start("pipApp window hides testApp") { - isInvisible(testApp.defaultWindowName) - } - end("testApp windows is shown") { - isVisible(testApp.defaultWindowName) - } - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - } - layersTrace { - val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90) - val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0) - start("pipApp layer hides testApp") { - hasVisibleRegion(pipApp.defaultWindowName, startingBounds) - isInvisible(testApp.defaultWindowName) - } - end("testApp layer covers fullscreen", enabled = false) { - hasVisibleRegion(testApp.defaultWindowName, endingBounds) - } - navBarLayerIsAlwaysVisible(bugId = 140855415) - statusBarLayerIsAlwaysVisible(bugId = 140855415) - } - } - } - } - - @Test - fun testSetRequestedOrientationWhilePinned() { - runFlicker(instrumentation) { - withTestName { "testSetRequestedOrientationWhilePinned" } - setup { - test { - removeAllTasksButHome() - device.wakeUpAndGoToHomeScreen() - // Launch the PiP activity fixed as landscape - pipApp.launchViaIntent(stringExtras = mapOf( - EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString(), - EXTRA_ENTER_PIP to "true")) - waitForAnimationComplete() - assertEquals(Surface.ROTATION_0, device.displayRotation) - } - } - transitions { - // Request that the orientation is set to landscape - broadcastActionTrigger.requestOrientationForPip(ORIENTATION_LANDSCAPE) - - // Launch the activity back into fullscreen and ensure that it is now in landscape - pipApp.launchViaIntent() - waitForAnimationComplete() - assertEquals(Surface.ROTATION_90, device.displayRotation) - } - teardown { - test { - removeAllTasksButHome() - } - } - assertions { - val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0) - val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90) - windowManagerTrace { - start("PIP window must remain inside display") { - coversAtMostRegion(pipApp.defaultWindowName, startingBounds) - } - end("pipApp shows on top") { - showsAppWindowOnTop(pipApp.defaultWindowName) - } - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - } - layersTrace { - start("PIP layer must remain inside display") { - coversAtMostRegion(startingBounds, pipApp.defaultWindowName) - } - end("pipApp layer covers fullscreen") { - hasVisibleRegion(pipApp.defaultWindowName, endingBounds) - } - navBarLayerIsAlwaysVisible(bugId = 140855415) - statusBarLayerIsAlwaysVisible(bugId = 140855415) - } - } - } - } - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<Array<Any>> { - val supportedRotations = intArrayOf(Surface.ROTATION_0) - return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt index a00c5f463a50..ade65ac8aa63 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.pip -import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry @@ -24,13 +23,9 @@ import com.android.server.wm.flicker.FlickerTestRunner import com.android.server.wm.flicker.FlickerTestRunnerFactory import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.helpers.buildTestTag import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.repetitions import com.android.server.wm.flicker.startRotation import com.android.wm.shell.flicker.helpers.FixedAppHelper -import com.android.wm.shell.flicker.helpers.PipAppHelper import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible @@ -38,7 +33,6 @@ import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible import com.android.server.wm.flicker.noUncoveredRegions import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP import org.junit.FixMethodOrder import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -48,80 +42,75 @@ import org.junit.runners.Parameterized * Test Pip Stack in bounds after rotations. * To run this test: `atest WMShellFlickerTests:PipRotationTest` */ -@Presubmit @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) class PipRotationTest( testSpec: FlickerTestRunnerFactory.TestSpec ) : FlickerTestRunner(testSpec) { - companion object { + companion object : PipTransitionBase(InstrumentationRegistry.getInstrumentation()) { @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<Array<Any>> { - val instrumentation = InstrumentationRegistry.getInstrumentation() - val testApp = FixedAppHelper(instrumentation) - val pipApp = PipAppHelper(instrumentation) - return FlickerTestRunnerFactory.getInstance().buildRotationTest(instrumentation, - supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90)) { - configuration -> - withTestName { buildTestTag("PipRotationTest", testApp, configuration) } - repeat { configuration.repetitions } - setup { - test { - AppTestBase.removeAllTasksButHome() - device.wakeUpAndGoToHomeScreen() - pipApp.launchViaIntent(stringExtras = mapOf( - EXTRA_ENTER_PIP to "true")) - testApp.launchViaIntent() - AppTestBase.waitForAnimationComplete() - } - eachRun { - setRotation(configuration.startRotation) - } - } - transitions { - setRotation(configuration.endRotation) + val fixedApp = FixedAppHelper(instrumentation) + val testSpec = getTransition(eachRun = false) { configuration -> + setup { + test { + fixedApp.launchViaIntent(wmHelper) + } + eachRun { + setRotation(configuration.startRotation) + } + } + transitions { + setRotation(configuration.endRotation) + } + teardown { + eachRun { + setRotation(Surface.ROTATION_0) + } + } + assertions { + val startingBounds = WindowUtils.getDisplayBounds(configuration.startRotation) + val endingBounds = WindowUtils.getDisplayBounds(configuration.endRotation) + + presubmit { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() } - teardown { - eachRun { - setRotation(Surface.ROTATION_0) - } - test { - AppTestBase.removeAllTasksButHome() - } + + layersTrace { + noUncoveredRegions(configuration.startRotation, + configuration.endRotation, allStates = false) } - assertions { - windowManagerTrace { - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - } - layersTrace { - navBarLayerIsAlwaysVisible(bugId = 140855415) - statusBarLayerIsAlwaysVisible(bugId = 140855415) - noUncoveredRegions(configuration.startRotation, - configuration.endRotation, allStates = false) - navBarLayerRotatesAndScales(configuration.startRotation, - configuration.endRotation, bugId = 140855415) - statusBarLayerRotatesScales(configuration.startRotation, - configuration.endRotation, bugId = 140855415) + } + + flaky { + layersTrace { + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible(bugId = 140855415) + navBarLayerRotatesAndScales(configuration.startRotation, + configuration.endRotation, bugId = 140855415) + statusBarLayerRotatesScales(configuration.startRotation, + configuration.endRotation, bugId = 140855415) + + start("appLayerRotates_StartingBounds", bugId = 140855415) { + hasVisibleRegion(fixedApp.defaultWindowName, startingBounds) + coversAtMostRegion(startingBounds, pipApp.defaultWindowName) } - layersTrace { - val startingBounds = WindowUtils.getDisplayBounds( - configuration.startRotation) - val endingBounds = WindowUtils.getDisplayBounds( - configuration.endRotation) - start("appLayerRotates_StartingBounds", bugId = 140855415) { - hasVisibleRegion(testApp.defaultWindowName, startingBounds) - coversAtMostRegion(startingBounds, pipApp.defaultWindowName) - } - end("appLayerRotates_EndingBounds", bugId = 140855415) { - hasVisibleRegion(testApp.defaultWindowName, endingBounds) - coversAtMostRegion(endingBounds, pipApp.defaultWindowName) - } + end("appLayerRotates_EndingBounds", bugId = 140855415) { + hasVisibleRegion(fixedApp.defaultWindowName, endingBounds) + coversAtMostRegion(endingBounds, pipApp.defaultWindowName) } } } + } + } + + return FlickerTestRunnerFactory.getInstance().buildRotationTest(instrumentation, + testSpec, supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90), + repetitions = 5) } } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt index 3e7eb134e627..f2d58997d1f2 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt @@ -17,28 +17,20 @@ package com.android.wm.shell.flicker.pip import android.view.Surface -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerTestRunner import com.android.server.wm.flicker.FlickerTestRunnerFactory import com.android.server.wm.flicker.focusChanges -import com.android.server.wm.flicker.helpers.buildTestTag -import com.android.server.wm.flicker.helpers.closePipWindow -import com.android.server.wm.flicker.helpers.expandPipWindow -import com.android.server.wm.flicker.helpers.hasPipWindow import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible import com.android.server.wm.flicker.noUncoveredRegions -import com.android.server.wm.flicker.repetitions import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.helpers.PipAppHelper import org.junit.FixMethodOrder import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -51,48 +43,29 @@ import org.junit.runners.Parameterized @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@FlakyTest(bugId = 152738416) class PipToAppTest( testSpec: FlickerTestRunnerFactory.TestSpec ) : FlickerTestRunner(testSpec) { - companion object { + companion object : PipTransitionBase(InstrumentationRegistry.getInstrumentation()) { @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): List<Array<Any>> { - val instrumentation = InstrumentationRegistry.getInstrumentation() - val testApp = PipAppHelper(instrumentation) - return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, - supportedRotations = listOf(Surface.ROTATION_0)) { configuration -> - withTestName { buildTestTag("exitPipModeToApp", testApp, configuration) } - repeat { configuration.repetitions } - setup { - test { - device.wakeUpAndGoToHomeScreen() - device.pressHome() - testApp.launchViaIntent(wmHelper) - } - eachRun { - this.setRotation(configuration.startRotation) - testApp.clickEnterPipButton() - device.hasPipWindow() - } + val testSpec = getTransition(eachRun = true) { configuration -> + setup { + eachRun { + this.setRotation(configuration.startRotation) } - teardown { - eachRun { - this.setRotation(Surface.ROTATION_0) - } - test { - if (device.hasPipWindow()) { - device.closePipWindow() - } - testApp.exit() - } - } - transitions { - device.expandPipWindow() - device.waitForIdle() + } + teardown { + eachRun { + this.setRotation(Surface.ROTATION_0) } - assertions { + } + transitions { + pipApp.expandPipWindowToApp(wmHelper) + } + assertions { + presubmit { windowManagerTrace { navBarWindowIsAlwaysVisible() statusBarWindowIsAlwaysVisible() @@ -100,34 +73,42 @@ class PipToAppTest( all("appReplacesPipWindow") { this.showsAppWindow(PIP_WINDOW_TITLE) .then() - .showsAppWindowOnTop(testApp.launcherName) + .showsAppWindowOnTop(pipApp.launcherName) } } layersTrace { - navBarLayerIsAlwaysVisible(bugId = 140855415) statusBarLayerIsAlwaysVisible() - noUncoveredRegions(configuration.startRotation, Surface.ROTATION_0, - enabled = false) - navBarLayerRotatesAndScales(configuration.startRotation, - Surface.ROTATION_0, bugId = 140855415) statusBarLayerRotatesScales(configuration.startRotation, Surface.ROTATION_0) all("appReplacesPipLayer") { this.showsLayer(PIP_WINDOW_TITLE) .then() - .showsLayer(testApp.launcherName) + .showsLayer(pipApp.launcherName) } } + } + + flaky { + layersTrace { + navBarLayerIsAlwaysVisible(bugId = 140855415) + noUncoveredRegions(configuration.startRotation, Surface.ROTATION_0) + navBarLayerRotatesAndScales(configuration.startRotation, + Surface.ROTATION_0, bugId = 140855415) + } eventLog { focusChanges( - "NexusLauncherActivity", testApp.launcherName, + "NexusLauncherActivity", pipApp.launcherName, "NexusLauncherActivity", bugId = 151179149) } } } + } + + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + testSpec, supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToHomeTest.kt index 5d3bc1388686..1b44377425db 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToHomeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToHomeTest.kt @@ -17,27 +17,20 @@ package com.android.wm.shell.flicker.pip import android.view.Surface -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerTestRunner import com.android.server.wm.flicker.FlickerTestRunnerFactory import com.android.server.wm.flicker.focusChanges -import com.android.server.wm.flicker.helpers.buildTestTag -import com.android.server.wm.flicker.helpers.closePipWindow -import com.android.server.wm.flicker.helpers.hasPipWindow import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible import com.android.server.wm.flicker.noUncoveredRegions -import com.android.server.wm.flicker.repetitions import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.helpers.PipAppHelper import org.junit.FixMethodOrder import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -50,50 +43,29 @@ import org.junit.runners.Parameterized @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@FlakyTest(bugId = 152738416) class PipToHomeTest( testSpec: FlickerTestRunnerFactory.TestSpec ) : FlickerTestRunner(testSpec) { - companion object { + companion object : PipTransitionBase(InstrumentationRegistry.getInstrumentation()) { @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): List<Array<Any>> { - val instrumentation = InstrumentationRegistry.getInstrumentation() - val testApp = PipAppHelper(instrumentation) - return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, - supportedRotations = listOf(Surface.ROTATION_0)) { configuration -> - withTestName { buildTestTag("exitPipModeToApp", testApp, configuration) } - repeat { configuration.repetitions } - setup { - test { - device.wakeUpAndGoToHomeScreen() - device.pressHome() - } - eachRun { - testApp.launchViaIntent(wmHelper) - this.setRotation(configuration.startRotation) - testApp.clickEnterPipButton() - device.hasPipWindow() - } + val testSpec = getTransition(eachRun = true) { configuration -> + setup { + eachRun { + this.setRotation(configuration.startRotation) } - teardown { - eachRun { - this.setRotation(Surface.ROTATION_0) - if (device.hasPipWindow()) { - device.closePipWindow() - } - } - test { - if (device.hasPipWindow()) { - device.closePipWindow() - } - testApp.exit() - } - } - transitions { - testApp.closePipWindow() + } + teardown { + eachRun { + this.setRotation(Surface.ROTATION_0) } - assertions { + } + transitions { + pipApp.closePipWindow(wmHelper) + } + assertions { + presubmit { windowManagerTrace { navBarWindowIsAlwaysVisible() statusBarWindowIsAlwaysVisible() @@ -106,12 +78,7 @@ class PipToHomeTest( } layersTrace { - navBarLayerIsAlwaysVisible(bugId = 140855415) statusBarLayerIsAlwaysVisible() - noUncoveredRegions(configuration.startRotation, Surface.ROTATION_0, - enabled = false) - navBarLayerRotatesAndScales(configuration.startRotation, - Surface.ROTATION_0, bugId = 140855415) statusBarLayerRotatesScales(configuration.startRotation, Surface.ROTATION_0) @@ -121,13 +88,28 @@ class PipToHomeTest( .hidesLayer(PIP_WINDOW_TITLE) } } + } + + postsubmit { + layersTrace { + navBarLayerIsAlwaysVisible() + noUncoveredRegions(configuration.startRotation, Surface.ROTATION_0) + navBarLayerRotatesAndScales(configuration.startRotation, + Surface.ROTATION_0) + } + } + flaky { eventLog { - focusChanges(testApp.launcherName, "NexusLauncherActivity", + focusChanges(pipApp.launcherName, "NexusLauncherActivity", bugId = 151179149) } } } + } + + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + testSpec, supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5) } } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransitionBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransitionBase.kt new file mode 100644 index 000000000000..b1e404e4c8e6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransitionBase.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2021 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.wm.shell.flicker.pip + +import android.app.Instrumentation +import android.content.Intent +import android.os.Bundle +import android.view.Surface +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.repetitions +import com.android.wm.shell.flicker.helpers.PipAppHelper +import com.android.wm.shell.flicker.removeAllTasksButHome +import com.android.wm.shell.flicker.testapp.Components + +abstract class PipTransitionBase(protected val instrumentation: Instrumentation) { + // Helper class to process test actions by broadcast. + protected class BroadcastActionTrigger(private val instrumentation: Instrumentation) { + private fun createIntentWithAction(broadcastAction: String): Intent { + return Intent(broadcastAction).setFlags(Intent.FLAG_RECEIVER_FOREGROUND) + } + + fun doAction(broadcastAction: String) { + instrumentation.context + .sendBroadcast(createIntentWithAction(broadcastAction)) + } + + fun requestOrientationForPip(orientation: Int) { + instrumentation.context.sendBroadcast( + createIntentWithAction(Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION) + .putExtra(Components.PipActivity.EXTRA_PIP_ORIENTATION, orientation.toString()) + ) + } + + companion object { + // Corresponds to ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + @JvmStatic + val ORIENTATION_LANDSCAPE = 0 + + // Corresponds to ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + @JvmStatic + val ORIENTATION_PORTRAIT = 1 + } + } + + protected val pipApp = PipAppHelper(instrumentation) + protected val broadcastActionTrigger = BroadcastActionTrigger(instrumentation) + + /** + * Gets a configuration that handles basic setup and teardown of pip tests + */ + protected val setupAndTeardown: FlickerBuilder.(Bundle) -> Unit + get() = { configuration -> + withTestName { buildTestTag(configuration) } + repeat { configuration.repetitions } + setup { + test { + removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + } + } + teardown { + eachRun { + setRotation(Surface.ROTATION_0) + } + test { + removeAllTasksButHome() + pipApp.exit() + } + } + } + + /** + * Gets a configuration that handles basic setup and teardown of pip tests and that + * launches the Pip app for test + * + * @param eachRun If the pip app should be launched in each run (otherwise only 1x per test) + * @param stringExtras Arguments to pass to the PIP launch intent + * @param extraSpec Addicional segment of flicker specification + */ + @JvmOverloads + open fun getTransition( + eachRun: Boolean, + stringExtras: Map<String, String> = mapOf(Components.PipActivity.EXTRA_ENTER_PIP to "true"), + extraSpec: FlickerBuilder.(Bundle) -> Unit = {} + ): FlickerBuilder.(Bundle) -> Unit { + return { configuration -> + setupAndTeardown(this, configuration) + + setup { + test { + removeAllTasksButHome() + if (!eachRun) { + pipApp.launchViaIntent(wmHelper, stringExtras = stringExtras) + wmHelper.waitPipWindowShown() + } + } + eachRun { + if (eachRun) { + pipApp.launchViaIntent(wmHelper, stringExtras = stringExtras) + wmHelper.waitPipWindowShown() + } + } + } + teardown { + eachRun { + if (eachRun) { + pipApp.exit() + } + } + test { + if (!eachRun) { + pipApp.exit() + } + removeAllTasksButHome() + } + } + + extraSpec(this, configuration) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt new file mode 100644 index 000000000000..c01bc94151e9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2020 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.wm.shell.flicker.pip + +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.pip.PipTransitionBase.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE +import com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION +import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP +import org.junit.Assert.assertEquals +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip with orientation changes. + * To run this test: `atest WMShellFlickerTests:PipOrientationTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class SetRequestedOrientationWhilePinnedTest( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : PipTransitionBase(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 1) { configuration -> + setupAndTeardown(this, configuration) + + setup { + eachRun { + // Launch the PiP activity fixed as landscape + pipApp.launchViaIntent(wmHelper, stringExtras = mapOf( + EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString(), + EXTRA_ENTER_PIP to "true")) + } + } + teardown { + eachRun { + pipApp.exit() + } + } + transitions { + // Request that the orientation is set to landscape + broadcastActionTrigger.requestOrientationForPip(ORIENTATION_LANDSCAPE) + + // Launch the activity back into fullscreen and + // ensure that it is now in landscape + pipApp.launchViaIntent(wmHelper) + wmHelper.waitForFullScreenApp(pipApp.component) + wmHelper.waitForRotation(Surface.ROTATION_90) + assertEquals(Surface.ROTATION_90, device.displayRotation) + } + assertions { + val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0) + val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90) + presubmit { + windowManagerTrace { + start("PIP window must remain inside display") { + coversAtMostRegion(pipApp.defaultWindowName, startingBounds) + } + end("pipApp shows on top") { + showsAppWindowOnTop(pipApp.defaultWindowName) + } + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + layersTrace { + start("PIP layer must remain inside display") { + coversAtMostRegion(startingBounds, pipApp.defaultWindowName) + } + end("pipApp layer covers fullscreen") { + hasVisibleRegion(pipApp.defaultWindowName, endingBounds) + } + } + } + + flaky { + layersTrace { + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible(bugId = 140855415) + } + } + } + } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index dca27328f192..c0ab20d9249f 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -15,7 +15,10 @@ android_test { name: "WMShellUnitTests", - srcs: ["**/*.java"], + srcs: [ + "**/*.java", + "**/*.kt", + ], static_libs: [ "WindowManager-Shell", diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index 862776ec7df2..a0e9f43218f2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -16,11 +16,14 @@ package com.android.wm.shell; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.Display.DEFAULT_DISPLAY; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; +import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN; import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_MULTI_WINDOW; import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_PIP; @@ -28,6 +31,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -48,6 +52,8 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.sizecompatui.SizeCompatUIController; +import com.android.wm.shell.startingsurface.StartingSurfaceDrawer; import org.junit.Before; import org.junit.Test; @@ -59,6 +65,9 @@ import java.util.ArrayList; /** * Tests for the shell task organizer. + * + * Build/Install/Run: + * atest WMShellUnitTests:ShellTaskOrganizerTests */ @SmallTest @RunWith(AndroidJUnit4.class) @@ -68,6 +77,10 @@ public class ShellTaskOrganizerTests { private ITaskOrganizerController mTaskOrganizerController; @Mock private Context mContext; + @Mock + private SizeCompatUIController mSizeCompatUI; + @Mock + private StartingSurfaceDrawer mStartingSurfaceDrawer; ShellTaskOrganizer mOrganizer; private final SyncTransactionQueue mSyncTransactionQueue = mock(SyncTransactionQueue.class); @@ -102,7 +115,8 @@ public class ShellTaskOrganizerTests { doReturn(ParceledListSlice.<TaskAppearedInfo>emptyList()) .when(mTaskOrganizerController).registerTaskOrganizer(any()); } catch (RemoteException e) {} - mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mTestExecutor, mContext)); + mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mTestExecutor, mContext, + mSizeCompatUI, mStartingSurfaceDrawer)); } @Test @@ -257,6 +271,37 @@ public class ShellTaskOrganizerTests { assertTrue(mwListener.appeared.contains(task2)); } + @Test + public void testOnSizeCompatActivityChanged() { + final RunningTaskInfo taskInfo1 = createTaskInfo(12, WINDOWING_MODE_FULLSCREEN); + taskInfo1.displayId = DEFAULT_DISPLAY; + taskInfo1.topActivityToken = mock(IBinder.class); + taskInfo1.topActivityInSizeCompat = false; + final TrackingTaskListener taskListener = new TrackingTaskListener(); + mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN); + mOrganizer.onTaskAppeared(taskInfo1, null); + + // sizeCompatActivity is null if top activity is not in size compat. + verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + null /* taskConfig */, null /* sizeCompatActivity*/, null /* taskListener */); + + // sizeCompatActivity is non-null if top activity is in size compat. + clearInvocations(mSizeCompatUI); + final RunningTaskInfo taskInfo2 = + createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); + taskInfo2.displayId = taskInfo1.displayId; + taskInfo2.topActivityToken = taskInfo1.topActivityToken; + taskInfo2.topActivityInSizeCompat = true; + mOrganizer.onTaskInfoChanged(taskInfo2); + verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + taskInfo1.configuration, taskInfo1.topActivityToken, taskListener); + + clearInvocations(mSizeCompatUI); + mOrganizer.onTaskVanished(taskInfo1); + verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + null /* taskConfig */, null /* sizeCompatActivity*/, null /* taskListener */); + } + private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt index 4bd9bed26a82..17ed396987af 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt @@ -26,7 +26,7 @@ import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringForce import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase +import com.android.wm.shell.ShellTestCase import com.android.wm.shell.animation.PhysicsAnimator.EndListener import com.android.wm.shell.animation.PhysicsAnimator.UpdateListener import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.clearAnimationUpdateFrames @@ -54,8 +54,7 @@ import org.mockito.MockitoAnnotations @TestableLooper.RunWithLooper @RunWith(AndroidTestingRunner::class) @SmallTest -@Ignore("Blocking presubmits - investigating in b/158697054") -class PhysicsAnimatorTest : SysuiTestCase() { +class PhysicsAnimatorTest : ShellTestCase() { private lateinit var viewGroup: ViewGroup private lateinit var testView: View private lateinit var testView2: View diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt index 4fab9a5496ec..dd1a6a5a281e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt @@ -20,12 +20,12 @@ import android.content.pm.LauncherApps import android.os.UserHandle import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest -import com.android.systemui.util.mockito.eq import com.android.wm.shell.ShellTestCase import junit.framework.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java index 495be4146f9d..21bc32c6563c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java @@ -27,7 +27,6 @@ import android.app.ActivityManager; import android.app.IActivityTaskManager; import android.content.ComponentName; import android.os.Handler; -import android.os.IBinder; import android.os.Message; import android.os.RemoteException; import android.testing.AndroidTestingRunner; @@ -234,14 +233,6 @@ public class TaskStackListenerImplTest { verify(mOtherCallback).onActivityRotation(eq(123)); } - @Test - public void testOnSizeCompatModeActivityChanged() { - IBinder b = mock(IBinder.class); - mImpl.onSizeCompatModeActivityChanged(123, b); - verify(mCallback).onSizeCompatModeActivityChanged(eq(123), eq(b)); - verify(mOtherCallback).onSizeCompatModeActivityChanged(eq(123), eq(b)); - } - /** * Handler that synchronously calls TaskStackListenerImpl#handleMessage() when it receives a * message. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt index fe536411d5ed..9f1ee6c92700 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt @@ -21,8 +21,8 @@ import android.view.MotionEvent import android.view.View import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.util.animation.PhysicsAnimatorTestUtils +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.animation.PhysicsAnimatorTestUtils import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -43,7 +43,7 @@ import org.mockito.Mockito.verifyNoMoreInteractions @TestableLooper.RunWithLooper @RunWith(AndroidTestingRunner::class) @SmallTest -class MagnetizedObjectTest : SysuiTestCase() { +class MagnetizedObjectTest : ShellTestCase() { /** Incrementing value for fake MotionEvent timestamps. */ private var time = 0L diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java index 79bdaf43f171..19ecc49513e5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java @@ -29,6 +29,10 @@ import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPL import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; @@ -40,6 +44,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.app.ActivityManager; @@ -61,7 +66,7 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.draganddrop.DragAndDropPolicy.Target; -import com.android.wm.shell.splitscreen.SplitScreen; +import com.android.wm.shell.splitscreen.SplitScreenController; import org.junit.Before; import org.junit.Test; @@ -86,11 +91,9 @@ public class DragAndDropPolicyTest { @Mock private ActivityTaskManager mActivityTaskManager; + // Both the split-screen and start interface. @Mock - private SplitScreen mSplitScreen; - - @Mock - private DragAndDropPolicy.Starter mStarter; + private SplitScreenController mSplitScreenStarter; private DisplayLayout mLandscapeDisplayLayout; private DisplayLayout mPortraitDisplayLayout; @@ -125,8 +128,8 @@ public class DragAndDropPolicyTest { mPortraitDisplayLayout = new DisplayLayout(info2, res, false, false); mInsets = Insets.of(0, 0, 0, 0); - mPolicy = new DragAndDropPolicy( - mContext, mActivityTaskManager, mSplitScreen, mStarter); + mPolicy = spy(new DragAndDropPolicy( + mContext, mActivityTaskManager, mSplitScreenStarter, mSplitScreenStarter)); mActivityClipData = createClipData(MIMETYPE_APPLICATION_ACTIVITY); mNonResizeableActivityClipData = createClipData(MIMETYPE_APPLICATION_ACTIVITY); setClipDataResizeable(mNonResizeableActivityClipData, false); @@ -191,7 +194,7 @@ public class DragAndDropPolicyTest { } private void setInSplitScreen(boolean inSplitscreen) { - doReturn(inSplitscreen).when(mSplitScreen).isSplitScreenVisible(); + doReturn(inSplitscreen).when(mSplitScreenStarter).isSplitScreenVisible(); } @Test @@ -202,7 +205,8 @@ public class DragAndDropPolicyTest { mPolicy.getTargets(mInsets), TYPE_FULLSCREEN); mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); - verify(mStarter).startIntent(any(), any()); + verify(mSplitScreenStarter).startIntent(any(), + eq(STAGE_TYPE_UNDEFINED), eq(STAGE_POSITION_UNDEFINED), any()); } @Test @@ -213,12 +217,13 @@ public class DragAndDropPolicyTest { mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); - verify(mStarter).exitSplitScreen(); - verify(mStarter).startIntent(any(), any()); - reset(mStarter); + verify(mSplitScreenStarter).startIntent(any(), + eq(STAGE_TYPE_UNDEFINED), eq(STAGE_POSITION_UNDEFINED), any()); + reset(mSplitScreenStarter); mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData); - verify(mStarter).startIntent(any(), any()); + verify(mSplitScreenStarter).startIntent(any(), + eq(STAGE_TYPE_SIDE), eq(STAGE_POSITION_BOTTOM_OR_RIGHT), any()); } @Test @@ -229,12 +234,13 @@ public class DragAndDropPolicyTest { mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); - verify(mStarter).exitSplitScreen(); - verify(mStarter).startIntent(any(), any()); - reset(mStarter); + verify(mSplitScreenStarter).startIntent(any(), + eq(STAGE_TYPE_UNDEFINED), eq(STAGE_POSITION_UNDEFINED), any()); + reset(mSplitScreenStarter); mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData); - verify(mStarter).startIntent(any(), any()); + verify(mSplitScreenStarter).startIntent(any(), + eq(STAGE_TYPE_SIDE), eq(STAGE_POSITION_BOTTOM_OR_RIGHT), any()); } @Test @@ -245,7 +251,8 @@ public class DragAndDropPolicyTest { mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); - verify(mStarter).startIntent(any(), any()); + verify(mSplitScreenStarter).startIntent(any(), + eq(STAGE_TYPE_UNDEFINED), eq(STAGE_POSITION_UNDEFINED), any()); } @Test @@ -256,7 +263,8 @@ public class DragAndDropPolicyTest { mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); - verify(mStarter).startIntent(any(), any()); + verify(mSplitScreenStarter).startIntent(any(), + eq(STAGE_TYPE_UNDEFINED), eq(STAGE_POSITION_UNDEFINED), any()); } @Test @@ -268,12 +276,14 @@ public class DragAndDropPolicyTest { mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); - verify(mStarter).startIntent(any(), any()); - reset(mStarter); + verify(mSplitScreenStarter).startIntent(any(), + eq(STAGE_TYPE_UNDEFINED), eq(STAGE_POSITION_UNDEFINED), any()); + reset(mSplitScreenStarter); // TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData); - verify(mStarter).startIntent(any(), any()); + verify(mSplitScreenStarter).startIntent(any(), + eq(STAGE_TYPE_SIDE), eq(STAGE_POSITION_BOTTOM_OR_RIGHT), any()); } @Test @@ -285,12 +295,14 @@ public class DragAndDropPolicyTest { mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); - verify(mStarter).startIntent(any(), any()); - reset(mStarter); + verify(mSplitScreenStarter).startIntent(any(), + eq(STAGE_TYPE_UNDEFINED), eq(STAGE_POSITION_UNDEFINED), any()); + reset(mSplitScreenStarter); // TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData); - verify(mStarter).startIntent(any(), any()); + verify(mSplitScreenStarter).startIntent(any(), + eq(STAGE_TYPE_SIDE), eq(STAGE_POSITION_BOTTOM_OR_RIGHT), any()); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java index c565a4cc2e28..3147dab1a0f8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java @@ -16,6 +16,9 @@ package com.android.wm.shell.pip; +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_90; + import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; @@ -24,6 +27,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; +import android.app.TaskInfo; import android.graphics.Matrix; import android.graphics.Rect; import android.testing.AndroidTestingRunner; @@ -33,6 +37,7 @@ import android.view.SurfaceControl; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayLayout; import org.junit.Before; import org.junit.Test; @@ -54,6 +59,9 @@ public class PipAnimationControllerTest extends ShellTestCase { private SurfaceControl mLeash; @Mock + private TaskInfo mTaskInfo; + + @Mock private PipAnimationController.PipAnimationCallback mPipAnimationCallback; @Before @@ -70,7 +78,7 @@ public class PipAnimationControllerTest extends ShellTestCase { @Test public void getAnimator_withAlpha_returnFloatAnimator() { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mLeash, new Rect(), 0f, 1f); + .getAnimator(mTaskInfo, mLeash, new Rect(), 0f, 1f); assertEquals("Expect ANIM_TYPE_ALPHA animation", animator.getAnimationType(), PipAnimationController.ANIM_TYPE_ALPHA); @@ -79,8 +87,8 @@ public class PipAnimationControllerTest extends ShellTestCase { @Test public void getAnimator_withBounds_returnBoundsAnimator() { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mLeash, new Rect(), new Rect(), new Rect(), null, - TRANSITION_DIRECTION_TO_PIP, 0); + .getAnimator(mTaskInfo, mLeash, new Rect(), new Rect(), new Rect(), null, + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); assertEquals("Expect ANIM_TYPE_BOUNDS animation", animator.getAnimationType(), PipAnimationController.ANIM_TYPE_BOUNDS); @@ -93,14 +101,14 @@ public class PipAnimationControllerTest extends ShellTestCase { final Rect endValue1 = new Rect(100, 100, 200, 200); final Rect endValue2 = new Rect(200, 200, 300, 300); final PipAnimationController.PipTransitionAnimator oldAnimator = mPipAnimationController - .getAnimator(mLeash, baseValue, startValue, endValue1, null, - TRANSITION_DIRECTION_TO_PIP, 0); + .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null, + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); oldAnimator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new); oldAnimator.start(); final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController - .getAnimator(mLeash, baseValue, startValue, endValue2, null, - TRANSITION_DIRECTION_TO_PIP, 0); + .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue2, null, + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); assertEquals("getAnimator with same type returns same animator", oldAnimator, newAnimator); @@ -111,19 +119,34 @@ public class PipAnimationControllerTest extends ShellTestCase { @Test public void getAnimator_setTransitionDirection() { PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mLeash, new Rect(), 0f, 1f) + .getAnimator(mTaskInfo, mLeash, new Rect(), 0f, 1f) .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP); assertEquals("Transition to PiP mode", animator.getTransitionDirection(), TRANSITION_DIRECTION_TO_PIP); animator = mPipAnimationController - .getAnimator(mLeash, new Rect(), 0f, 1f) + .getAnimator(mTaskInfo, mLeash, new Rect(), 0f, 1f) .setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP); assertEquals("Transition to fullscreen mode", animator.getTransitionDirection(), TRANSITION_DIRECTION_LEAVE_PIP); } @Test + public void pipTransitionAnimator_rotatedEndValue() { + final Rect startBounds = new Rect(200, 700, 400, 800); + final Rect endBounds = new Rect(0, 0, 500, 1000); + final PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController + .getAnimator(mTaskInfo, mLeash, null, startBounds, endBounds, null, + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_90); + // Apply fraction 1 to compute the end value. + animator.applySurfaceControlTransaction(mLeash, new DummySurfaceControlTx(), 1); + final Rect rotatedEndBounds = new Rect(endBounds); + DisplayLayout.rotateBounds(rotatedEndBounds, endBounds, ROTATION_90); + + assertEquals("Expect 90 degree rotated bounds", rotatedEndBounds, animator.mCurrentValue); + } + + @Test @SuppressWarnings("unchecked") public void pipTransitionAnimator_updateEndValue() { final Rect baseValue = new Rect(0, 0, 100, 100); @@ -131,8 +154,8 @@ public class PipAnimationControllerTest extends ShellTestCase { final Rect endValue1 = new Rect(100, 100, 200, 200); final Rect endValue2 = new Rect(200, 200, 300, 300); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mLeash, baseValue, startValue, endValue1, null, - TRANSITION_DIRECTION_TO_PIP, 0); + .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null, + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); animator.updateEndValue(endValue2); @@ -145,24 +168,24 @@ public class PipAnimationControllerTest extends ShellTestCase { final Rect startValue = new Rect(0, 0, 100, 100); final Rect endValue = new Rect(100, 100, 200, 200); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mLeash, baseValue, startValue, endValue, null, - TRANSITION_DIRECTION_TO_PIP, 0); + .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null, + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); animator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new); animator.setPipAnimationCallback(mPipAnimationCallback); // onAnimationStart triggers onPipAnimationStart animator.onAnimationStart(animator); - verify(mPipAnimationCallback).onPipAnimationStart(animator); + verify(mPipAnimationCallback).onPipAnimationStart(mTaskInfo, animator); // onAnimationCancel triggers onPipAnimationCancel animator.onAnimationCancel(animator); - verify(mPipAnimationCallback).onPipAnimationCancel(animator); + verify(mPipAnimationCallback).onPipAnimationCancel(mTaskInfo, animator); // onAnimationEnd triggers onPipAnimationEnd animator.onAnimationEnd(animator); - verify(mPipAnimationCallback).onPipAnimationEnd(any(SurfaceControl.Transaction.class), - eq(animator)); + verify(mPipAnimationCallback).onPipAnimationEnd(eq(mTaskInfo), + any(SurfaceControl.Transaction.class), eq(animator)); } /** diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index 7a810a1742d7..d10c03677d30 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -18,27 +18,23 @@ package com.android.wm.shell.pip; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.PictureInPictureParams; import android.content.ComponentName; import android.content.pm.ActivityInfo; -import android.graphics.Rect; import android.os.RemoteException; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.Rational; import android.util.Size; -import android.view.Display; import android.view.DisplayInfo; import android.window.WindowContainerToken; @@ -47,9 +43,9 @@ import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; +import com.android.wm.shell.pip.phone.PhonePipMenuController; import org.junit.Before; import org.junit.Test; @@ -69,14 +65,17 @@ public class PipTaskOrganizerTest extends ShellTestCase { private PipTaskOrganizer mSpiedPipTaskOrganizer; @Mock private DisplayController mMockdDisplayController; - @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm; + @Mock private PhonePipMenuController mMockPhonePipMenuController; + @Mock private PipAnimationController mMockPipAnimationController; + @Mock private PipTransitionController mMockPipTransitionController; @Mock private PipSurfaceTransactionHelper mMockPipSurfaceTransactionHelper; @Mock private PipUiEventLogger mMockPipUiEventLogger; - @Mock private Optional<LegacySplitScreen> mMockOptionalSplitScreen; + @Mock private Optional<LegacySplitScreenController> mMockOptionalSplitScreen; @Mock private ShellTaskOrganizer mMockShellTaskOrganizer; private TestShellExecutor mMainExecutor; private PipBoundsState mPipBoundsState; + private PipBoundsAlgorithm mPipBoundsAlgorithm; private ComponentName mComponent1; private ComponentName mComponent2; @@ -87,10 +86,12 @@ public class PipTaskOrganizerTest extends ShellTestCase { mComponent1 = new ComponentName(mContext, "component1"); mComponent2 = new ComponentName(mContext, "component2"); mPipBoundsState = new PipBoundsState(mContext); + mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState); mMainExecutor = new TestShellExecutor(); mSpiedPipTaskOrganizer = spy(new PipTaskOrganizer(mContext, mPipBoundsState, - mMockPipBoundsAlgorithm, mMockPhonePipMenuController, - mMockPipSurfaceTransactionHelper, mMockOptionalSplitScreen, mMockdDisplayController, + mPipBoundsAlgorithm, mMockPhonePipMenuController, + mMockPipAnimationController, mMockPipSurfaceTransactionHelper, + mMockPipTransitionController, mMockOptionalSplitScreen, mMockdDisplayController, mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor)); mMainExecutor.flushAll(); preparePipTaskOrg(); @@ -117,7 +118,7 @@ public class PipTaskOrganizerTest extends ShellTestCase { @Test public void startSwipePipToHome_updatesLastPipComponentName() { - mSpiedPipTaskOrganizer.startSwipePipToHome(mComponent1, null, null); + mSpiedPipTaskOrganizer.startSwipePipToHome(mComponent1, null, createPipParams(null)); assertEquals(mComponent1, mPipBoundsState.getLastPipComponentName()); } @@ -126,7 +127,8 @@ public class PipTaskOrganizerTest extends ShellTestCase { public void startSwipePipToHome_updatesOverrideMinSize() { final Size minSize = new Size(100, 80); - mSpiedPipTaskOrganizer.startSwipePipToHome(mComponent1, createActivityInfo(minSize), null); + mSpiedPipTaskOrganizer.startSwipePipToHome(mComponent1, createActivityInfo(minSize), + createPipParams(null)); assertEquals(minSize, mPipBoundsState.getOverrideMinSize()); } @@ -200,9 +202,6 @@ public class PipTaskOrganizerTest extends ShellTestCase { final DisplayInfo info = new DisplayInfo(); mPipBoundsState.setDisplayLayout(new DisplayLayout(info, mContext.getResources(), true, true)); - when(mMockPipBoundsAlgorithm.getEntryDestinationBounds()).thenReturn(new Rect()); - when(mMockPipBoundsAlgorithm.getAdjustedDestinationBounds(any(), anyFloat())) - .thenReturn(new Rect()); mSpiedPipTaskOrganizer.setOneShotAnimationType(PipAnimationController.ANIM_TYPE_ALPHA); doNothing().when(mSpiedPipTaskOrganizer).enterPipWithAlphaAnimation(any(), anyLong()); doNothing().when(mSpiedPipTaskOrganizer).scheduleAnimateResizePip(any(), anyInt(), any()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index 62ffac4fbd3f..cfe84639d24e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -46,6 +46,7 @@ import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipTransitionController; import org.junit.Before; import org.junit.Test; @@ -68,6 +69,7 @@ public class PipControllerTest extends ShellTestCase { @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm; @Mock private PipMediaController mMockPipMediaController; @Mock private PipTaskOrganizer mMockPipTaskOrganizer; + @Mock private PipTransitionController mMockPipTransitionController; @Mock private PipTouchHandler mMockPipTouchHandler; @Mock private WindowManagerShellWrapper mMockWindowManagerShellWrapper; @Mock private PipBoundsState mMockPipBoundsState; @@ -80,8 +82,8 @@ public class PipControllerTest extends ShellTestCase { mPipController = new PipController(mContext, mMockDisplayController, mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipBoundsState, mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, - mMockPipTouchHandler, mMockWindowManagerShellWrapper, mMockTaskStackListener, - mMockExecutor); + mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, + mMockTaskStackListener, mMockExecutor); doAnswer(invocation -> { ((Runnable) invocation.getArgument(0)).run(); return null; @@ -90,7 +92,7 @@ public class PipControllerTest extends ShellTestCase { @Test public void instantiatePipController_registersPipTransitionCallback() { - verify(mMockPipTaskOrganizer).registerPipTransitionCallback(any()); + verify(mMockPipTransitionController).registerPipTransitionCallback(any()); } @Test @@ -113,8 +115,8 @@ public class PipControllerTest extends ShellTestCase { assertNull(PipController.create(spyContext, mMockDisplayController, mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipBoundsState, mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, - mMockPipTouchHandler, mMockWindowManagerShellWrapper, mMockTaskStackListener, - mMockExecutor)); + mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, + mMockTaskStackListener, mMockExecutor)); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java index b4cfbc281d61..449ad88f6532 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java @@ -38,6 +38,7 @@ import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipUiEventLogger; import org.junit.Before; @@ -67,6 +68,9 @@ public class PipTouchHandlerTest extends ShellTestCase { private PipTaskOrganizer mPipTaskOrganizer; @Mock + private PipTransitionController mMockPipTransitionController; + + @Mock private FloatingContentCoordinator mFloatingContentCoordinator; @Mock @@ -98,7 +102,8 @@ public class PipTouchHandlerTest extends ShellTestCase { mPipSnapAlgorithm = new PipSnapAlgorithm(); mPipTouchHandler = new PipTouchHandler(mContext, mPhonePipMenuController, mPipBoundsAlgorithm, mPipBoundsState, mPipTaskOrganizer, - mFloatingContentCoordinator, mPipUiEventLogger, mMainExecutor); + mMockPipTransitionController, mFloatingContentCoordinator, mPipUiEventLogger, + mMainExecutor); mMotionHelper = Mockito.spy(mPipTouchHandler.getMotionHelper()); mPipResizeGestureHandler = Mockito.spy(mPipTouchHandler.getPipResizeGestureHandler()); mPipTouchHandler.setPipMotionHelper(mMotionHelper); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButtonTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButtonTest.java new file mode 100644 index 000000000000..d9086a6ccdc1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButtonTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2021 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.wm.shell.sizecompatui; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.content.res.Configuration; +import android.os.IBinder; +import android.testing.AndroidTestingRunner; +import android.view.LayoutInflater; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link SizeCompatRestartButton}. + * + * Build/Install/Run: + * atest WMShellUnitTests:SizeCompatRestartButtonTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class SizeCompatRestartButtonTest extends ShellTestCase { + + @Mock private SyncTransactionQueue mSyncTransactionQueue; + @Mock private IBinder mActivityToken; + @Mock private ShellTaskOrganizer.TaskListener mTaskListener; + @Mock private DisplayLayout mDisplayLayout; + + private SizeCompatUILayout mLayout; + private SizeCompatRestartButton mButton; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + final int taskId = 1; + mLayout = new SizeCompatUILayout(mSyncTransactionQueue, mContext, new Configuration(), + taskId, mActivityToken, mTaskListener, mDisplayLayout, false /* hasShownHint*/); + mButton = (SizeCompatRestartButton) + LayoutInflater.from(mContext).inflate(R.layout.size_compat_ui, null); + mButton.inject(mLayout); + + spyOn(mLayout); + spyOn(mButton); + doNothing().when(mButton).showHint(); + } + + @Test + public void testOnClick() { + doNothing().when(mLayout).onRestartButtonClicked(); + + mButton.onClick(mButton); + + verify(mLayout).onRestartButtonClicked(); + } + + @Test + public void testOnLongClick() { + verify(mButton, never()).showHint(); + + mButton.onLongClick(mButton); + + verify(mButton).showHint(); + } + + @Test + public void testOnAttachedToWindow_showHint() { + mLayout.mShouldShowHint = false; + mButton.onAttachedToWindow(); + + verify(mButton, never()).showHint(); + + mLayout.mShouldShowHint = true; + mButton.onAttachedToWindow(); + + verify(mButton).showHint(); + } + + @Test + public void testRemove() { + mButton.remove(); + + verify(mButton).dismissHint(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java new file mode 100644 index 000000000000..806a90b7832a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2021 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.wm.shell.sizecompatui; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.content.res.Configuration; +import android.os.IBinder; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link SizeCompatUIController}. + * + * Build/Install/Run: + * atest WMShellUnitTests:SizeCompatUIControllerTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class SizeCompatUIControllerTest extends ShellTestCase { + private static final int DISPLAY_ID = 0; + private static final int TASK_ID = 12; + + private SizeCompatUIController mController; + private @Mock DisplayController mMockDisplayController; + private @Mock DisplayLayout mMockDisplayLayout; + private @Mock DisplayImeController mMockImeController; + private @Mock IBinder mMockActivityToken; + private @Mock ShellTaskOrganizer.TaskListener mMockTaskListener; + private @Mock SyncTransactionQueue mMockSyncQueue; + private @Mock SizeCompatUILayout mMockLayout; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + doReturn(mMockDisplayLayout).when(mMockDisplayController).getDisplayLayout(anyInt()); + doReturn(DISPLAY_ID).when(mMockLayout).getDisplayId(); + doReturn(TASK_ID).when(mMockLayout).getTaskId(); + mController = new SizeCompatUIController(mContext, mMockDisplayController, + mMockImeController, mMockSyncQueue) { + @Override + SizeCompatUILayout createLayout(Context context, int displayId, int taskId, + Configuration taskConfig, IBinder activityToken, + ShellTaskOrganizer.TaskListener taskListener) { + return mMockLayout; + } + }; + spyOn(mController); + } + + @Test + public void testListenerRegistered() { + verify(mMockDisplayController).addDisplayWindowListener(mController); + verify(mMockImeController).addPositionProcessor(mController); + } + + @Test + public void testOnSizeCompatInfoChanged() { + final Configuration taskConfig = new Configuration(); + + // Verify that the restart button is added with non-null size compat info. + mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, + mMockActivityToken, mMockTaskListener); + + verify(mController).createLayout(any(), eq(DISPLAY_ID), eq(TASK_ID), eq(taskConfig), + eq(mMockActivityToken), eq(mMockTaskListener)); + + // Verify that the restart button is updated with non-null new size compat info. + final Configuration newTaskConfig = new Configuration(); + mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, newTaskConfig, + mMockActivityToken, mMockTaskListener); + + verify(mMockLayout).updateSizeCompatInfo(taskConfig, mMockActivityToken, mMockTaskListener, + false /* isImeShowing */); + + // Verify that the restart button is removed with null size compat info. + mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, null, null, mMockTaskListener); + + verify(mMockLayout).release(); + } + + @Test + public void testOnDisplayRemoved() { + final Configuration taskConfig = new Configuration(); + mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, + mMockActivityToken, mMockTaskListener); + + mController.onDisplayRemoved(DISPLAY_ID + 1); + + verify(mMockLayout, never()).release(); + + mController.onDisplayRemoved(DISPLAY_ID); + + verify(mMockLayout).release(); + } + + @Test + public void testOnDisplayConfigurationChanged() { + final Configuration taskConfig = new Configuration(); + mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, + mMockActivityToken, mMockTaskListener); + + final Configuration newTaskConfig = new Configuration(); + mController.onDisplayConfigurationChanged(DISPLAY_ID + 1, newTaskConfig); + + verify(mMockLayout, never()).updateDisplayLayout(any()); + + mController.onDisplayConfigurationChanged(DISPLAY_ID, newTaskConfig); + + verify(mMockLayout).updateDisplayLayout(mMockDisplayLayout); + } + + @Test + public void testChangeButtonVisibilityOnImeShowHide() { + final Configuration taskConfig = new Configuration(); + mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, + mMockActivityToken, mMockTaskListener); + + mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */); + + verify(mMockLayout).updateImeVisibility(true); + + mController.onImeVisibilityChanged(DISPLAY_ID, false /* isShowing */); + + verify(mMockLayout).updateImeVisibility(false); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUILayoutTest.java new file mode 100644 index 000000000000..236db44bdce0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUILayoutTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2021 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.wm.shell.sizecompatui; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.app.ActivityClient; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.IBinder; +import android.testing.AndroidTestingRunner; +import android.view.DisplayInfo; +import android.view.SurfaceControl; +import android.view.View; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link SizeCompatUILayout}. + * + * Build/Install/Run: + * atest WMShellUnitTests:SizeCompatUILayoutTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class SizeCompatUILayoutTest extends ShellTestCase { + + private static final int TASK_ID = 1; + + @Mock private SyncTransactionQueue mSyncTransactionQueue; + @Mock private IBinder mActivityToken; + @Mock private ShellTaskOrganizer.TaskListener mTaskListener; + @Mock private DisplayLayout mDisplayLayout; + @Mock private SizeCompatRestartButton mButton; + private Configuration mTaskConfig; + + private SizeCompatUILayout mLayout; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTaskConfig = new Configuration(); + + mLayout = new SizeCompatUILayout(mSyncTransactionQueue, mContext, new Configuration(), + TASK_ID, mActivityToken, mTaskListener, mDisplayLayout, false /* hasShownHint*/); + + spyOn(mLayout); + spyOn(mLayout.mWindowManager); + doReturn(mButton).when(mLayout.mWindowManager).createSizeCompatUI(); + } + + @Test + public void testCreateSizeCompatButton() { + // Not create button if IME is showing. + mLayout.createSizeCompatButton(true /* isImeShowing */); + + verify(mLayout.mWindowManager, never()).createSizeCompatUI(); + assertNull(mLayout.mButton); + + mLayout.createSizeCompatButton(false /* isImeShowing */); + + verify(mLayout.mWindowManager).createSizeCompatUI(); + assertNotNull(mLayout.mButton); + } + + @Test + public void testRelease() { + mLayout.createSizeCompatButton(false /* isImeShowing */); + + mLayout.release(); + + assertNull(mLayout.mButton); + verify(mButton).remove(); + verify(mLayout.mWindowManager).release(); + } + + @Test + public void testUpdateSizeCompatInfo() { + mLayout.createSizeCompatButton(false /* isImeShowing */); + + // No diff + clearInvocations(mLayout); + mLayout.updateSizeCompatInfo(mTaskConfig, mActivityToken, mTaskListener, + false /* isImeShowing */); + + verify(mLayout, never()).updateSurfacePosition(); + verify(mLayout, never()).release(); + verify(mLayout, never()).createSizeCompatButton(anyBoolean()); + + // Change task listener, recreate button. + clearInvocations(mLayout); + final ShellTaskOrganizer.TaskListener newTaskListener = mock( + ShellTaskOrganizer.TaskListener.class); + mLayout.updateSizeCompatInfo(mTaskConfig, mActivityToken, newTaskListener, + false /* isImeShowing */); + + verify(mLayout).release(); + verify(mLayout).createSizeCompatButton(anyBoolean()); + + // Change task bounds, update position. + clearInvocations(mLayout); + final Configuration newTaskConfiguration = new Configuration(); + newTaskConfiguration.windowConfiguration.setBounds(new Rect(0, 1000, 0, 2000)); + mLayout.updateSizeCompatInfo(newTaskConfiguration, mActivityToken, newTaskListener, + false /* isImeShowing */); + + verify(mLayout).updateSurfacePosition(); + } + + @Test + public void testUpdateDisplayLayout() { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = 1000; + displayInfo.logicalHeight = 2000; + final DisplayLayout displayLayout1 = new DisplayLayout(displayInfo, + mContext.getResources(), false, false); + + mLayout.updateDisplayLayout(displayLayout1); + verify(mLayout).updateSurfacePosition(); + + // No update if the display bounds is the same. + clearInvocations(mLayout); + final DisplayLayout displayLayout2 = new DisplayLayout(displayInfo, + mContext.getResources(), false, false); + mLayout.updateDisplayLayout(displayLayout2); + verify(mLayout, never()).updateSurfacePosition(); + } + + @Test + public void testUpdateImeVisibility() { + // Create button if it is not created. + mLayout.mButton = null; + mLayout.updateImeVisibility(false /* isImeShowing */); + + verify(mLayout).createSizeCompatButton(false /* isImeShowing */); + + // Hide button if ime is shown. + clearInvocations(mLayout); + doReturn(View.VISIBLE).when(mButton).getVisibility(); + mLayout.updateImeVisibility(true /* isImeShowing */); + + verify(mLayout, never()).createSizeCompatButton(anyBoolean()); + verify(mButton).setVisibility(View.GONE); + + // Show button if ime is not shown. + doReturn(View.GONE).when(mButton).getVisibility(); + mLayout.updateImeVisibility(false /* isImeShowing */); + + verify(mLayout, never()).createSizeCompatButton(anyBoolean()); + verify(mButton).setVisibility(View.VISIBLE); + } + + @Test + public void testAttachToParentSurface() { + final SurfaceControl.Builder b = new SurfaceControl.Builder(); + mLayout.attachToParentSurface(b); + + verify(mTaskListener).attachChildSurfaceToTask(TASK_ID, b); + } + + @Test + public void testOnRestartButtonClicked() { + spyOn(ActivityClient.getInstance()); + doNothing().when(ActivityClient.getInstance()).restartActivityProcessIfVisible(any()); + + mLayout.onRestartButtonClicked(); + + verify(ActivityClient.getInstance()).restartActivityProcessIfVisible(mActivityToken); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index 168e0df35fe1..d2d18129d071 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -19,7 +19,7 @@ package com.android.wm.shell.splitscreen; import static android.view.Display.DEFAULT_DISPLAY; import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER; -import static com.android.wm.shell.splitscreen.SplitScreen.SIDE_STAGE_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_BOTTOM_OR_RIGHT; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -71,7 +71,7 @@ public class StageCoordinatorTests extends ShellTestCase { public void testMoveToSideStage() { final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); - mStageCoordinator.moveToSideStage(task, SIDE_STAGE_POSITION_BOTTOM_OR_RIGHT); + mStageCoordinator.moveToSideStage(task, STAGE_POSITION_BOTTOM_OR_RIGHT); verify(mMainStage).activate(any(Rect.class), any(WindowContainerTransaction.class)); verify(mSideStage).addTask(eq(task), any(Rect.class), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java index c9537afa37ef..de7d6c74bb06 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java @@ -40,6 +40,7 @@ import android.testing.TestableContext; import android.view.View; import android.view.WindowManager; import android.view.WindowMetrics; +import android.window.SplashScreenView; import android.window.StartingWindowInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -79,7 +80,8 @@ public class StartingSurfaceDrawerTests { @Override protected void postAddWindow(int taskId, IBinder appToken, - View view, WindowManager wm, WindowManager.LayoutParams params) { + View view, WindowManager wm, WindowManager.LayoutParams params, + SplashScreenView splashScreenView) { // listen for addView mAddWindowForTask = taskId; mViewThemeResId = view.getContext().getThemeResId(); @@ -125,7 +127,8 @@ public class StartingSurfaceDrawerTests { createWindowInfo(taskId, android.R.style.Theme); mStartingSurfaceDrawer.addStartingWindow(windowInfo, mBinder); waitHandlerIdle(mainLoop); - verify(mStartingSurfaceDrawer).postAddWindow(eq(taskId), eq(mBinder), any(), any(), any()); + verify(mStartingSurfaceDrawer).postAddWindow( + eq(taskId), eq(mBinder), any(), any(), any(), any()); assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId); mStartingSurfaceDrawer.removeStartingWindow(windowInfo.taskInfo.taskId); @@ -142,7 +145,8 @@ public class StartingSurfaceDrawerTests { createWindowInfo(taskId, 0); mStartingSurfaceDrawer.addStartingWindow(windowInfo, mBinder); waitHandlerIdle(mainLoop); - verify(mStartingSurfaceDrawer).postAddWindow(eq(taskId), eq(mBinder), any(), any(), any()); + verify(mStartingSurfaceDrawer).postAddWindow( + eq(taskId), eq(mBinder), any(), any(), any(), any()); assertNotEquals(mStartingSurfaceDrawer.mViewThemeResId, 0); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java index 414a0a778d93..27e5f51d88b8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java @@ -47,6 +47,7 @@ import android.window.TaskSnapshot; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.startingsurface.TaskSnapshotWindow; import org.junit.Test; @@ -83,7 +84,7 @@ public class TaskSnapshotWindowTest { createTaskDescription(Color.WHITE, Color.RED, Color.BLUE), 0 /* appearance */, windowFlags /* windowFlags */, 0 /* privateWindowFlags */, taskBounds, ORIENTATION_PORTRAIT, ACTIVITY_TYPE_STANDARD, new InsetsState(), - null /* clearWindow */); + null /* clearWindow */, new TestShellExecutor()); } private TaskSnapshot createTaskSnapshot(int width, int height, Point taskSize, diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index b54f7d81274c..4b4284a0c745 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -41,6 +41,7 @@ cc_library { "AssetDir.cpp", "AssetManager.cpp", "AssetManager2.cpp", + "AssetsProvider.cpp", "AttributeResolution.cpp", "ChunkIterator.cpp", "ConfigDescription.cpp", diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp index 011a0de8031f..9c743cea592a 100755 --- a/libs/androidfw/ApkAssets.cpp +++ b/libs/androidfw/ApkAssets.cpp @@ -16,533 +16,153 @@ #include "androidfw/ApkAssets.h" -#include <algorithm> - #include "android-base/errors.h" -#include "android-base/file.h" #include "android-base/logging.h" -#include "android-base/stringprintf.h" -#include "android-base/unique_fd.h" -#include "android-base/utf8.h" -#include "utils/Compat.h" -#include "ziparchive/zip_archive.h" - -#include "androidfw/Asset.h" -#include "androidfw/Idmap.h" -#include "androidfw/misc.h" -#include "androidfw/Util.h" namespace android { using base::SystemErrorCodeToString; using base::unique_fd; -static const std::string kResourcesArsc("resources.arsc"); - -ApkAssets::ApkAssets(std::unique_ptr<const AssetsProvider> assets_provider, - std::string path, - time_t last_mod_time, - package_property_t property_flags) - : assets_provider_(std::move(assets_provider)), - path_(std::move(path)), - last_mod_time_(last_mod_time), - property_flags_(property_flags) { +constexpr const char* kResourcesArsc = "resources.arsc"; + +ApkAssets::ApkAssets(std::unique_ptr<Asset> resources_asset, + std::unique_ptr<LoadedArsc> loaded_arsc, + std::unique_ptr<AssetsProvider> assets, + package_property_t property_flags, + std::unique_ptr<Asset> idmap_asset, + std::unique_ptr<LoadedIdmap> loaded_idmap) + : resources_asset_(std::move(resources_asset)), + loaded_arsc_(std::move(loaded_arsc)), + assets_provider_(std::move(assets)), + property_flags_(property_flags), + idmap_asset_(std::move(idmap_asset)), + loaded_idmap_(std::move(loaded_idmap)) {} + +std::unique_ptr<ApkAssets> ApkAssets::Load(const std::string& path, package_property_t flags) { + return Load(ZipAssetsProvider::Create(path), flags); } -// Provides asset files from a zip file. -class ZipAssetsProvider : public AssetsProvider { - public: - ~ZipAssetsProvider() override = default; - - static std::unique_ptr<const AssetsProvider> Create(const std::string& path) { - ::ZipArchiveHandle unmanaged_handle; - const int32_t result = ::OpenArchive(path.c_str(), &unmanaged_handle); - if (result != 0) { - LOG(ERROR) << "Failed to open APK '" << path << "' " << ::ErrorCodeString(result); - ::CloseArchive(unmanaged_handle); - return {}; - } - - return std::unique_ptr<AssetsProvider>(new ZipAssetsProvider(path, path, unmanaged_handle)); - } - - static std::unique_ptr<const AssetsProvider> Create( - unique_fd fd, const std::string& friendly_name, const off64_t offset = 0, - const off64_t length = ApkAssets::kUnknownLength) { - - ::ZipArchiveHandle unmanaged_handle; - const int32_t result = (length == ApkAssets::kUnknownLength) - ? ::OpenArchiveFd(fd.release(), friendly_name.c_str(), &unmanaged_handle) - : ::OpenArchiveFdRange(fd.release(), friendly_name.c_str(), &unmanaged_handle, length, - offset); - - if (result != 0) { - LOG(ERROR) << "Failed to open APK '" << friendly_name << "' through FD with offset " << offset - << " and length " << length << ": " << ::ErrorCodeString(result); - ::CloseArchive(unmanaged_handle); - return {}; - } - - return std::unique_ptr<AssetsProvider>(new ZipAssetsProvider({}, friendly_name, - unmanaged_handle)); - } - - // Iterate over all files and directories within the zip. The order of iteration is not - // guaranteed to be the same as the order of elements in the central directory but is stable for a - // given zip file. - bool ForEachFile(const std::string& root_path, - const std::function<void(const StringPiece&, FileType)>& f) const override { - // If this is a resource loader from an .arsc, there will be no zip handle - if (zip_handle_ == nullptr) { - return false; - } - - std::string root_path_full = root_path; - if (root_path_full.back() != '/') { - root_path_full += '/'; - } - - void* cookie; - if (::StartIteration(zip_handle_.get(), &cookie, root_path_full, "") != 0) { - return false; - } - - std::string name; - ::ZipEntry entry{}; - - // We need to hold back directories because many paths will contain them and we want to only - // surface one. - std::set<std::string> dirs{}; - - int32_t result; - while ((result = ::Next(cookie, &entry, &name)) == 0) { - StringPiece full_file_path(name); - StringPiece leaf_file_path = full_file_path.substr(root_path_full.size()); - - if (!leaf_file_path.empty()) { - auto iter = std::find(leaf_file_path.begin(), leaf_file_path.end(), '/'); - if (iter != leaf_file_path.end()) { - std::string dir = - leaf_file_path.substr(0, std::distance(leaf_file_path.begin(), iter)).to_string(); - dirs.insert(std::move(dir)); - } else { - f(leaf_file_path, kFileTypeRegular); - } - } - } - ::EndIteration(cookie); - - // Now present the unique directories. - for (const std::string& dir : dirs) { - f(dir, kFileTypeDirectory); - } - - // -1 is end of iteration, anything else is an error. - return result == -1; - } - - protected: - std::unique_ptr<Asset> OpenInternal( - const std::string& path, Asset::AccessMode mode, bool* file_exists) const override { - if (file_exists) { - *file_exists = false; - } - - ::ZipEntry entry; - int32_t result = ::FindEntry(zip_handle_.get(), path, &entry); - if (result != 0) { - return {}; - } - - if (file_exists) { - *file_exists = true; - } - - const int fd = ::GetFileDescriptor(zip_handle_.get()); - const off64_t fd_offset = ::GetFileDescriptorOffset(zip_handle_.get()); - incfs::IncFsFileMap asset_map; - if (entry.method == kCompressDeflated) { - if (!asset_map.Create(fd, entry.offset + fd_offset, entry.compressed_length, GetPath())) { - LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << friendly_name_ << "'"; - return {}; - } - - std::unique_ptr<Asset> asset = - Asset::createFromCompressedMap(std::move(asset_map), entry.uncompressed_length, mode); - if (asset == nullptr) { - LOG(ERROR) << "Failed to decompress '" << path << "' in APK '" << friendly_name_ << "'"; - return {}; - } - return asset; - } - - if (!asset_map.Create(fd, entry.offset + fd_offset, entry.uncompressed_length, GetPath())) { - LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << friendly_name_ << "'"; - return {}; - } - - unique_fd ufd; - if (!GetPath()) { - // If the `path` is not set, create a new `fd` for the new Asset to own in order to create - // new file descriptors using Asset::openFileDescriptor. If the path is set, it will be used - // to create new file descriptors. - ufd = unique_fd(dup(fd)); - if (!ufd.ok()) { - LOG(ERROR) << "Unable to dup fd '" << path << "' in APK '" << friendly_name_ << "'"; - return {}; - } - } - - auto asset = Asset::createFromUncompressedMap(std::move(asset_map), mode, std::move(ufd)); - if (asset == nullptr) { - LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << friendly_name_ << "'"; - return {}; - } - return asset; - } - - private: - DISALLOW_COPY_AND_ASSIGN(ZipAssetsProvider); - - explicit ZipAssetsProvider(std::string path, - std::string friendly_name, - ZipArchiveHandle unmanaged_handle) - : zip_handle_(unmanaged_handle, ::CloseArchive), - path_(std::move(path)), - friendly_name_(std::move(friendly_name)) { } - - const char* GetPath() const { - return path_.empty() ? nullptr : path_.c_str(); - } - - using ZipArchivePtr = std::unique_ptr<ZipArchive, void (*)(ZipArchiveHandle)>; - ZipArchivePtr zip_handle_; - std::string path_; - std::string friendly_name_; -}; - -class DirectoryAssetsProvider : AssetsProvider { - public: - ~DirectoryAssetsProvider() override = default; - - static std::unique_ptr<const AssetsProvider> Create(const std::string& path) { - struct stat sb{}; - const int result = stat(path.c_str(), &sb); - if (result == -1) { - LOG(ERROR) << "Failed to find directory '" << path << "'."; - return nullptr; - } - - if (!S_ISDIR(sb.st_mode)) { - LOG(ERROR) << "Path '" << path << "' is not a directory."; - return nullptr; - } - - return std::unique_ptr<AssetsProvider>(new DirectoryAssetsProvider(path)); - } - - protected: - std::unique_ptr<Asset> OpenInternal( - const std::string& path, Asset::AccessMode /* mode */, bool* file_exists) const override { - const std::string resolved_path = ResolvePath(path); - if (file_exists) { - struct stat sb{}; - const int result = stat(resolved_path.c_str(), &sb); - *file_exists = result != -1 && S_ISREG(sb.st_mode); - } - - return ApkAssets::CreateAssetFromFile(resolved_path); - } - - private: - DISALLOW_COPY_AND_ASSIGN(DirectoryAssetsProvider); - - explicit DirectoryAssetsProvider(std::string path) : path_(std::move(path)) { } - - inline std::string ResolvePath(const std::string& path) const { - return base::StringPrintf("%s%c%s", path_.c_str(), OS_PATH_SEPARATOR, path.c_str()); - } - - const std::string path_; -}; - -// AssetProvider implementation that does not provide any assets. Used for ApkAssets::LoadEmpty. -class EmptyAssetsProvider : public AssetsProvider { - public: - EmptyAssetsProvider() = default; - ~EmptyAssetsProvider() override = default; - - protected: - std::unique_ptr<Asset> OpenInternal(const std::string& /*path */, - Asset::AccessMode /* mode */, - bool* file_exists) const override { - if (file_exists) { - *file_exists = false; - } - return nullptr; - } - - private: - DISALLOW_COPY_AND_ASSIGN(EmptyAssetsProvider); -}; - -// AssetProvider implementation -class MultiAssetsProvider : public AssetsProvider { - public: - ~MultiAssetsProvider() override = default; - - static std::unique_ptr<const AssetsProvider> Create( - std::unique_ptr<const AssetsProvider> child, std::unique_ptr<const AssetsProvider> parent) { - CHECK(parent != nullptr) << "parent provider must not be null"; - return (!child) ? std::move(parent) - : std::unique_ptr<const AssetsProvider>(new MultiAssetsProvider( - std::move(child), std::move(parent))); - } - - bool ForEachFile(const std::string& root_path, - const std::function<void(const StringPiece&, FileType)>& f) const override { - // TODO: Only call the function once for files defined in the parent and child - return child_->ForEachFile(root_path, f) && parent_->ForEachFile(root_path, f); - } - - protected: - std::unique_ptr<Asset> OpenInternal( - const std::string& path, Asset::AccessMode mode, bool* file_exists) const override { - auto asset = child_->Open(path, mode, file_exists); - return (asset) ? std::move(asset) : parent_->Open(path, mode, file_exists); - } - - private: - DISALLOW_COPY_AND_ASSIGN(MultiAssetsProvider); - - MultiAssetsProvider(std::unique_ptr<const AssetsProvider> child, - std::unique_ptr<const AssetsProvider> parent) - : child_(std::move(child)), parent_(std::move(parent)) { } - - std::unique_ptr<const AssetsProvider> child_; - std::unique_ptr<const AssetsProvider> parent_; -}; - -// Opens the archive using the file path. Calling CloseArchive on the zip handle will close the -// file. -std::unique_ptr<const ApkAssets> ApkAssets::Load( - const std::string& path, const package_property_t flags, - std::unique_ptr<const AssetsProvider> override_asset) { - auto assets = ZipAssetsProvider::Create(path); - return (assets) ? LoadImpl(std::move(assets), path, flags, std::move(override_asset)) - : nullptr; -} - -// Opens the archive using the file file descriptor with the specified file offset and read length. -// If the `assume_ownership` parameter is 'true' calling CloseArchive will close the file. -std::unique_ptr<const ApkAssets> ApkAssets::LoadFromFd( - unique_fd fd, const std::string& friendly_name, const package_property_t flags, - std::unique_ptr<const AssetsProvider> override_asset, const off64_t offset, - const off64_t length) { - CHECK(length >= kUnknownLength) << "length must be greater than or equal to " << kUnknownLength; - CHECK(length != kUnknownLength || offset == 0) << "offset must be 0 if length is " - << kUnknownLength; - - auto assets = ZipAssetsProvider::Create(std::move(fd), friendly_name, offset, length); - return (assets) ? LoadImpl(std::move(assets), friendly_name, flags, std::move(override_asset)) - : nullptr; +std::unique_ptr<ApkAssets> ApkAssets::LoadFromFd(base::unique_fd fd, + const std::string& debug_name, + package_property_t flags, + off64_t offset, + off64_t len) { + return Load(ZipAssetsProvider::Create(std::move(fd), debug_name, offset, len), flags); } -std::unique_ptr<const ApkAssets> ApkAssets::LoadTable( - const std::string& path, const package_property_t flags, - std::unique_ptr<const AssetsProvider> override_asset) { - - auto assets = CreateAssetFromFile(path); - return (assets) ? LoadTableImpl(std::move(assets), path, flags, std::move(override_asset)) - : nullptr; +std::unique_ptr<ApkAssets> ApkAssets::Load(std::unique_ptr<AssetsProvider> assets, + package_property_t flags) { + return LoadImpl(std::move(assets), flags, nullptr /* idmap_asset */, nullptr /* loaded_idmap */); } -std::unique_ptr<const ApkAssets> ApkAssets::LoadTableFromFd( - unique_fd fd, const std::string& friendly_name, const package_property_t flags, - std::unique_ptr<const AssetsProvider> override_asset, const off64_t offset, - const off64_t length) { - - auto assets = CreateAssetFromFd(std::move(fd), nullptr /* path */, offset, length); - return (assets) ? LoadTableImpl(std::move(assets), friendly_name, flags, - std::move(override_asset)) - : nullptr; +std::unique_ptr<ApkAssets> ApkAssets::LoadTable(std::unique_ptr<Asset> resources_asset, + std::unique_ptr<AssetsProvider> assets, + package_property_t flags) { + if (resources_asset == nullptr) { + return {}; + } + return LoadImpl(std::move(resources_asset), std::move(assets), flags, nullptr /* idmap_asset */, + nullptr /* loaded_idmap */); } -std::unique_ptr<const ApkAssets> ApkAssets::LoadOverlay(const std::string& idmap_path, - const package_property_t flags) { +std::unique_ptr<ApkAssets> ApkAssets::LoadOverlay(const std::string& idmap_path, + package_property_t flags) { CHECK((flags & PROPERTY_LOADER) == 0U) << "Cannot load RROs through loaders"; - std::unique_ptr<Asset> idmap_asset = CreateAssetFromFile(idmap_path); + auto idmap_asset = AssetsProvider::CreateAssetFromFile(idmap_path); if (idmap_asset == nullptr) { + LOG(ERROR) << "failed to read IDMAP " << idmap_path; return {}; } - const StringPiece idmap_data( - reinterpret_cast<const char*>(idmap_asset->getBuffer(true /*wordAligned*/)), - static_cast<size_t>(idmap_asset->getLength())); - std::unique_ptr<const LoadedIdmap> loaded_idmap = LoadedIdmap::Load(idmap_path, idmap_data); + StringPiece idmap_data(reinterpret_cast<const char*>(idmap_asset->getBuffer(true /* aligned */)), + static_cast<size_t>(idmap_asset->getLength())); + auto loaded_idmap = LoadedIdmap::Load(idmap_path, idmap_data); if (loaded_idmap == nullptr) { LOG(ERROR) << "failed to load IDMAP " << idmap_path; return {}; } - - auto overlay_path = std::string(loaded_idmap->OverlayApkPath()); - auto assets = ZipAssetsProvider::Create(overlay_path); - return (assets) ? LoadImpl(std::move(assets), overlay_path, flags | PROPERTY_OVERLAY, - nullptr /* override_asset */, std::move(idmap_asset), - std::move(loaded_idmap)) - : nullptr; -} - -std::unique_ptr<const ApkAssets> ApkAssets::LoadFromDir( - const std::string& path, const package_property_t flags, - std::unique_ptr<const AssetsProvider> override_asset) { - - auto assets = DirectoryAssetsProvider::Create(path); - return (assets) ? LoadImpl(std::move(assets), path, flags, std::move(override_asset)) - : nullptr; -} -std::unique_ptr<const ApkAssets> ApkAssets::LoadEmpty( - const package_property_t flags, std::unique_ptr<const AssetsProvider> override_asset) { - - auto assets = (override_asset) ? std::move(override_asset) - : std::unique_ptr<const AssetsProvider>(new EmptyAssetsProvider()); - std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(std::move(assets), "empty" /* path */, - -1 /* last_mod-time */, flags)); - loaded_apk->loaded_arsc_ = LoadedArsc::CreateEmpty(); - // Need to force a move for mingw32. - return std::move(loaded_apk); -} - -std::unique_ptr<Asset> ApkAssets::CreateAssetFromFile(const std::string& path) { - unique_fd fd(base::utf8::open(path.c_str(), O_RDONLY | O_BINARY | O_CLOEXEC)); - if (!fd.ok()) { - LOG(ERROR) << "Failed to open file '" << path << "': " << SystemErrorCodeToString(errno); + std::unique_ptr<AssetsProvider> overlay_assets; + const std::string overlay_path(loaded_idmap->OverlayApkPath()); + if (IsFabricatedOverlay(overlay_path)) { + // Fabricated overlays do not contain resource definitions. All of the overlay resource values + // are defined inline in the idmap. + overlay_assets = EmptyAssetsProvider::Create(); + } else { + // The overlay should be an APK. + overlay_assets = ZipAssetsProvider::Create(overlay_path); + } + if (overlay_assets == nullptr) { return {}; } - return CreateAssetFromFd(std::move(fd), path.c_str()); + return LoadImpl(std::move(overlay_assets), flags | PROPERTY_OVERLAY, std::move(idmap_asset), + std::move(loaded_idmap)); } -std::unique_ptr<Asset> ApkAssets::CreateAssetFromFd(base::unique_fd fd, - const char* path, - off64_t offset, - off64_t length) { - CHECK(length >= kUnknownLength) << "length must be greater than or equal to " << kUnknownLength; - CHECK(length != kUnknownLength || offset == 0) << "offset must be 0 if length is " - << kUnknownLength; - if (length == kUnknownLength) { - length = lseek64(fd, 0, SEEK_END); - if (length < 0) { - LOG(ERROR) << "Failed to get size of file '" << ((path) ? path : "anon") << "': " - << SystemErrorCodeToString(errno); - return {}; - } - } - - incfs::IncFsFileMap file_map; - if (!file_map.Create(fd, offset, static_cast<size_t>(length), path)) { - LOG(ERROR) << "Failed to mmap file '" << ((path) ? path : "anon") << "': " - << SystemErrorCodeToString(errno); +std::unique_ptr<ApkAssets> ApkAssets::LoadImpl(std::unique_ptr<AssetsProvider> assets, + package_property_t property_flags, + std::unique_ptr<Asset> idmap_asset, + std::unique_ptr<LoadedIdmap> loaded_idmap) { + if (assets == nullptr) { return {}; } - // If `path` is set, do not pass ownership of the `fd` to the new Asset since - // Asset::openFileDescriptor can use `path` to create new file descriptors. - return Asset::createFromUncompressedMap(std::move(file_map), - Asset::AccessMode::ACCESS_RANDOM, - (path) ? base::unique_fd(-1) : std::move(fd)); -} - -std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl( - std::unique_ptr<const AssetsProvider> assets, const std::string& path, - package_property_t property_flags, std::unique_ptr<const AssetsProvider> override_assets, - std::unique_ptr<Asset> idmap_asset, std::unique_ptr<const LoadedIdmap> idmap) { - - const time_t last_mod_time = getFileModDate(path.c_str()); - // Open the resource table via mmap unless it is compressed. This logic is taken care of by Open. bool resources_asset_exists = false; - auto resources_asset_ = assets->Open(kResourcesArsc, Asset::AccessMode::ACCESS_BUFFER, - &resources_asset_exists); - - assets = MultiAssetsProvider::Create(std::move(override_assets), std::move(assets)); - - // Wrap the handle in a unique_ptr so it gets automatically closed. - std::unique_ptr<ApkAssets> - loaded_apk(new ApkAssets(std::move(assets), path, last_mod_time, property_flags)); - - if (!resources_asset_exists) { - loaded_apk->loaded_arsc_ = LoadedArsc::CreateEmpty(); - return std::move(loaded_apk); - } - - loaded_apk->resources_asset_ = std::move(resources_asset_); - if (!loaded_apk->resources_asset_) { - LOG(ERROR) << "Failed to open '" << kResourcesArsc << "' in APK '" << path << "'."; + auto resources_asset = assets->Open(kResourcesArsc, Asset::AccessMode::ACCESS_BUFFER, + &resources_asset_exists); + if (resources_asset == nullptr && resources_asset_exists) { + LOG(ERROR) << "Failed to open '" << kResourcesArsc << "' in APK '" << assets->GetDebugName() + << "'."; return {}; } - // Must retain ownership of the IDMAP Asset so that all pointers to its mmapped data remain valid. - loaded_apk->idmap_asset_ = std::move(idmap_asset); - loaded_apk->loaded_idmap_ = std::move(idmap); + return LoadImpl(std::move(resources_asset), std::move(assets), property_flags, + std::move(idmap_asset), std::move(loaded_idmap)); +} - const auto data = loaded_apk->resources_asset_->getIncFsBuffer(true /* aligned */); - const size_t length = loaded_apk->resources_asset_->getLength(); - if (!data || length == 0) { - LOG(ERROR) << "Failed to read '" << kResourcesArsc << "' data in APK '" << path << "'."; +std::unique_ptr<ApkAssets> ApkAssets::LoadImpl(std::unique_ptr<Asset> resources_asset, + std::unique_ptr<AssetsProvider> assets, + package_property_t property_flags, + std::unique_ptr<Asset> idmap_asset, + std::unique_ptr<LoadedIdmap> loaded_idmap) { + if (assets == nullptr ) { return {}; } - loaded_apk->loaded_arsc_ = LoadedArsc::Load(data, length, loaded_apk->loaded_idmap_.get(), - property_flags); - if (!loaded_apk->loaded_arsc_) { - LOG(ERROR) << "Failed to load '" << kResourcesArsc << "' in APK '" << path << "'."; - return {}; + std::unique_ptr<LoadedArsc> loaded_arsc; + if (resources_asset != nullptr) { + const auto data = resources_asset->getIncFsBuffer(true /* aligned */); + const size_t length = resources_asset->getLength(); + if (!data || length == 0) { + LOG(ERROR) << "Failed to read resources table in APK '" << assets->GetDebugName() << "'."; + return {}; + } + loaded_arsc = LoadedArsc::Load(data, length, loaded_idmap.get(), property_flags); + } else { + loaded_arsc = LoadedArsc::CreateEmpty(); } - // Need to force a move for mingw32. - return std::move(loaded_apk); -} - -std::unique_ptr<const ApkAssets> ApkAssets::LoadTableImpl( - std::unique_ptr<Asset> resources_asset, const std::string& path, - package_property_t property_flags, std::unique_ptr<const AssetsProvider> override_assets) { - - const time_t last_mod_time = getFileModDate(path.c_str()); - - auto assets = (override_assets) ? std::move(override_assets) - : std::unique_ptr<AssetsProvider>(new EmptyAssetsProvider()); - - std::unique_ptr<ApkAssets> loaded_apk( - new ApkAssets(std::move(assets), path, last_mod_time, property_flags)); - loaded_apk->resources_asset_ = std::move(resources_asset); - - const auto data = loaded_apk->resources_asset_->getIncFsBuffer(true /* aligned */); - const size_t length = loaded_apk->resources_asset_->getLength(); - if (!data || length == 0) { - LOG(ERROR) << "Failed to read resources table data in '" << path << "'."; + if (loaded_arsc == nullptr) { + LOG(ERROR) << "Failed to load resources table in APK '" << assets->GetDebugName() << "'."; return {}; } - loaded_apk->loaded_arsc_ = LoadedArsc::Load(data, length, nullptr /* loaded_idmap */, - property_flags); - if (loaded_apk->loaded_arsc_ == nullptr) { - LOG(ERROR) << "Failed to read resources table in '" << path << "'."; - return {}; - } + return std::unique_ptr<ApkAssets>(new ApkAssets(std::move(resources_asset), + std::move(loaded_arsc), std::move(assets), + property_flags, std::move(idmap_asset), + std::move(loaded_idmap))); +} - // Need to force a move for mingw32. - return std::move(loaded_apk); +const std::string& ApkAssets::GetPath() const { + return assets_provider_->GetDebugName(); } bool ApkAssets::IsUpToDate() const { - if (IsLoader()) { - // Loaders are invalidated by the app, not the system, so assume they are up to date. - return true; - } - return (!loaded_idmap_ || loaded_idmap_->IsUpToDate()) && - last_mod_time_ == getFileModDate(path_.c_str()); + // Loaders are invalidated by the app, not the system, so assume they are up to date. + return IsLoader() || ((!loaded_idmap_ || loaded_idmap_->IsUpToDate()) + && assets_provider_->IsUpToDate()); } - } // namespace android diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 03ab62f48870..36bde5ccf267 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -102,9 +102,8 @@ AssetManager2::AssetManager2() { memset(&configuration_, 0, sizeof(configuration_)); } -bool AssetManager2::SetApkAssets(const std::vector<const ApkAssets*>& apk_assets, - bool invalidate_caches) { - apk_assets_ = apk_assets; +bool AssetManager2::SetApkAssets(std::vector<const ApkAssets*> apk_assets, bool invalidate_caches) { + apk_assets_ = std::move(apk_assets); BuildDynamicRefTable(); RebuildFilterList(); if (invalidate_caches) { @@ -137,6 +136,36 @@ void AssetManager2::BuildDynamicRefTable() { // 0x01 is reserved for the android package. int next_package_id = 0x02; for (const ApkAssets* apk_assets : sorted_apk_assets) { + std::shared_ptr<OverlayDynamicRefTable> overlay_ref_table; + if (auto loaded_idmap = apk_assets->GetLoadedIdmap(); loaded_idmap != nullptr) { + // The target package must precede the overlay package in the apk assets paths in order + // to take effect. + auto iter = apk_assets_package_ids.find(std::string(loaded_idmap->TargetApkPath())); + if (iter == apk_assets_package_ids.end()) { + LOG(INFO) << "failed to find target package for overlay " + << loaded_idmap->OverlayApkPath(); + } else { + uint8_t target_package_id = iter->second; + + // Create a special dynamic reference table for the overlay to rewrite references to + // overlay resources as references to the target resources they overlay. + overlay_ref_table = std::make_shared<OverlayDynamicRefTable>( + loaded_idmap->GetOverlayDynamicRefTable(target_package_id)); + + // Add the overlay resource map to the target package's set of overlays. + const uint8_t target_idx = package_ids_[target_package_id]; + CHECK(target_idx != 0xff) << "overlay target '" << loaded_idmap->TargetApkPath() + << "'added to apk_assets_package_ids but does not have an" + << " assigned package group"; + + PackageGroup& target_package_group = package_groups_[target_idx]; + target_package_group.overlays_.push_back( + ConfiguredOverlay{loaded_idmap->GetTargetResourcesMap(target_package_id, + overlay_ref_table.get()), + apk_assets_cookies[apk_assets]}); + } + } + const LoadedArsc* loaded_arsc = apk_assets->GetLoadedArsc(); for (const std::unique_ptr<const LoadedPackage>& package : loaded_arsc->GetPackages()) { // Get the package ID or assign one if a shared library. @@ -147,50 +176,25 @@ void AssetManager2::BuildDynamicRefTable() { package_id = package->GetPackageId(); } - // Add the mapping for package ID to index if not present. uint8_t idx = package_ids_[package_id]; if (idx == 0xff) { + // Add the mapping for package ID to index if not present. package_ids_[package_id] = idx = static_cast<uint8_t>(package_groups_.size()); - package_groups_.push_back({}); - - if (apk_assets->IsOverlay()) { - // The target package must precede the overlay package in the apk assets paths in order - // to take effect. - const auto& loaded_idmap = apk_assets->GetLoadedIdmap(); - auto target_package_iter = apk_assets_package_ids.find( - std::string(loaded_idmap->TargetApkPath())); - if (target_package_iter == apk_assets_package_ids.end()) { - LOG(INFO) << "failed to find target package for overlay " - << loaded_idmap->OverlayApkPath(); - } else { - const uint8_t target_package_id = target_package_iter->second; - const uint8_t target_idx = package_ids_[target_package_id]; - CHECK(target_idx != 0xff) << "overlay added to apk_assets_package_ids but does not" - << " have an assigned package group"; - - PackageGroup& target_package_group = package_groups_[target_idx]; - - // Create a special dynamic reference table for the overlay to rewrite references to - // overlay resources as references to the target resources they overlay. - auto overlay_table = std::make_shared<OverlayDynamicRefTable>( - loaded_idmap->GetOverlayDynamicRefTable(target_package_id)); - package_groups_.back().dynamic_ref_table = overlay_table; - - // Add the overlay resource map to the target package's set of overlays. - target_package_group.overlays_.push_back( - ConfiguredOverlay{loaded_idmap->GetTargetResourcesMap(target_package_id, - overlay_table.get()), - apk_assets_cookies[apk_assets]}); - } + PackageGroup& new_group = package_groups_.emplace_back(); + + if (overlay_ref_table != nullptr) { + // If this package is from an overlay, use a dynamic reference table that can rewrite + // overlay resource ids to their corresponding target resource ids. + new_group.dynamic_ref_table = overlay_ref_table; } - DynamicRefTable* ref_table = package_groups_.back().dynamic_ref_table.get(); + DynamicRefTable* ref_table = new_group.dynamic_ref_table.get(); ref_table->mAssignedPackageId = package_id; ref_table->mAppAsLib = package->IsDynamic() && package->GetPackageId() == 0x7f; } - PackageGroup* package_group = &package_groups_[idx]; // Add the package and to the set of packages with the same ID. + PackageGroup* package_group = &package_groups_[idx]; package_group->packages_.push_back(ConfiguredPackage{package.get(), {}}); package_group->cookies_.push_back(apk_assets_cookies[apk_assets]); @@ -578,7 +582,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( const PackageGroup& package_group = package_groups_[package_idx]; auto result = FindEntryInternal(package_group, type_idx, entry_idx, *desired_config, - stop_at_first_match, ignore_configuration); + stop_at_first_match, ignore_configuration); if (UNLIKELY(!result.has_value())) { return base::unexpected(result.error()); } diff --git a/libs/androidfw/AssetsProvider.cpp b/libs/androidfw/AssetsProvider.cpp new file mode 100644 index 000000000000..f3c48f7f9fc8 --- /dev/null +++ b/libs/androidfw/AssetsProvider.cpp @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2021 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. + */ + +#include "androidfw/AssetsProvider.h" + +#include <sys/stat.h> + +#include <android-base/errors.h> +#include <android-base/stringprintf.h> +#include <android-base/utf8.h> +#include <ziparchive/zip_archive.h> + +namespace android { +namespace { +constexpr const char* kEmptyDebugString = "<empty>"; +} // namespace + +std::unique_ptr<Asset> AssetsProvider::Open(const std::string& path, Asset::AccessMode mode, + bool* file_exists) const { + return OpenInternal(path, mode, file_exists); +} + +std::unique_ptr<Asset> AssetsProvider::CreateAssetFromFile(const std::string& path) { + base::unique_fd fd(base::utf8::open(path.c_str(), O_RDONLY | O_CLOEXEC)); + if (!fd.ok()) { + LOG(ERROR) << "Failed to open file '" << path << "': " << base::SystemErrorCodeToString(errno); + return {}; + } + + return CreateAssetFromFd(std::move(fd), path.c_str()); +} + +std::unique_ptr<Asset> AssetsProvider::CreateAssetFromFd(base::unique_fd fd, + const char* path, + off64_t offset, + off64_t length) { + CHECK(length >= kUnknownLength) << "length must be greater than or equal to " << kUnknownLength; + CHECK(length != kUnknownLength || offset == 0) << "offset must be 0 if length is " + << kUnknownLength; + if (length == kUnknownLength) { + length = lseek64(fd, 0, SEEK_END); + if (length < 0) { + LOG(ERROR) << "Failed to get size of file '" << ((path) ? path : "anon") << "': " + << base::SystemErrorCodeToString(errno); + return {}; + } + } + + incfs::IncFsFileMap file_map; + if (!file_map.Create(fd, offset, static_cast<size_t>(length), path)) { + LOG(ERROR) << "Failed to mmap file '" << ((path != nullptr) ? path : "anon") << "': " + << base::SystemErrorCodeToString(errno); + return {}; + } + + // If `path` is set, do not pass ownership of the `fd` to the new Asset since + // Asset::openFileDescriptor can use `path` to create new file descriptors. + return Asset::createFromUncompressedMap(std::move(file_map), + Asset::AccessMode::ACCESS_RANDOM, + (path != nullptr) ? base::unique_fd(-1) : std::move(fd)); +} + +ZipAssetsProvider::PathOrDebugName::PathOrDebugName(std::string&& value, bool is_path) + : value_(std::forward<std::string>(value)), is_path_(is_path) {} + +const std::string* ZipAssetsProvider::PathOrDebugName::GetPath() const { + return is_path_ ? &value_ : nullptr; +} + +const std::string& ZipAssetsProvider::PathOrDebugName::GetDebugName() const { + return value_; +} + +ZipAssetsProvider::ZipAssetsProvider(ZipArchiveHandle handle, PathOrDebugName&& path, + time_t last_mod_time) + : zip_handle_(handle, ::CloseArchive), + name_(std::forward<PathOrDebugName>(path)), + last_mod_time_(last_mod_time) {} + +std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path) { + ZipArchiveHandle handle; + if (int32_t result = OpenArchive(path.c_str(), &handle); result != 0) { + LOG(ERROR) << "Failed to open APK '" << path << "': " << ::ErrorCodeString(result); + CloseArchive(handle); + return {}; + } + + struct stat sb{.st_mtime = -1}; + if (stat(path.c_str(), &sb) < 0) { + // Stat requires execute permissions on all directories path to the file. If the process does + // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will + // always have to return true. + LOG(WARNING) << "Failed to stat file '" << path << "': " + << base::SystemErrorCodeToString(errno); + } + + return std::unique_ptr<ZipAssetsProvider>( + new ZipAssetsProvider(handle, PathOrDebugName{std::move(path), + true /* is_path */}, sb.st_mtime)); +} + +std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, + std::string friendly_name, + off64_t offset, + off64_t len) { + ZipArchiveHandle handle; + const int released_fd = fd.release(); + const int32_t result = (len == AssetsProvider::kUnknownLength) + ? ::OpenArchiveFd(released_fd, friendly_name.c_str(), &handle) + : ::OpenArchiveFdRange(released_fd, friendly_name.c_str(), &handle, len, offset); + + if (result != 0) { + LOG(ERROR) << "Failed to open APK '" << friendly_name << "' through FD with offset " << offset + << " and length " << len << ": " << ::ErrorCodeString(result); + CloseArchive(handle); + return {}; + } + + struct stat sb{.st_mtime = -1}; + if (fstat(released_fd, &sb) < 0) { + // Stat requires execute permissions on all directories path to the file. If the process does + // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will + // always have to return true. + LOG(WARNING) << "Failed to fstat file '" << friendly_name << "': " + << base::SystemErrorCodeToString(errno); + } + + return std::unique_ptr<ZipAssetsProvider>( + new ZipAssetsProvider(handle, PathOrDebugName{std::move(friendly_name), + false /* is_path */}, sb.st_mtime)); +} + +std::unique_ptr<Asset> ZipAssetsProvider::OpenInternal(const std::string& path, + Asset::AccessMode mode, + bool* file_exists) const { + if (file_exists != nullptr) { + *file_exists = false; + } + + ZipEntry entry; + if (FindEntry(zip_handle_.get(), path, &entry) != 0) { + return {}; + } + + if (file_exists != nullptr) { + *file_exists = true; + } + + const int fd = GetFileDescriptor(zip_handle_.get()); + const off64_t fd_offset = GetFileDescriptorOffset(zip_handle_.get()); + incfs::IncFsFileMap asset_map; + if (entry.method == kCompressDeflated) { + if (!asset_map.Create(fd, entry.offset + fd_offset, entry.compressed_length, + name_.GetDebugName().c_str())) { + LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << name_.GetDebugName() + << "'"; + return {}; + } + + std::unique_ptr<Asset> asset = + Asset::createFromCompressedMap(std::move(asset_map), entry.uncompressed_length, mode); + if (asset == nullptr) { + LOG(ERROR) << "Failed to decompress '" << path << "' in APK '" << name_.GetDebugName() + << "'"; + return {}; + } + return asset; + } + + if (!asset_map.Create(fd, entry.offset + fd_offset, entry.uncompressed_length, + name_.GetDebugName().c_str())) { + LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << name_.GetDebugName() << "'"; + return {}; + } + + base::unique_fd ufd; + if (name_.GetPath() == nullptr) { + // If the zip name does not represent a path, create a new `fd` for the new Asset to own in + // order to create new file descriptors using Asset::openFileDescriptor. If the zip name is a + // path, it will be used to create new file descriptors. + ufd = base::unique_fd(dup(fd)); + if (!ufd.ok()) { + LOG(ERROR) << "Unable to dup fd '" << path << "' in APK '" << name_.GetDebugName() << "'"; + return {}; + } + } + + auto asset = Asset::createFromUncompressedMap(std::move(asset_map), mode, std::move(ufd)); + if (asset == nullptr) { + LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << name_.GetDebugName() << "'"; + return {}; + } + return asset; +} + +bool ZipAssetsProvider::ForEachFile(const std::string& root_path, + const std::function<void(const StringPiece&, FileType)>& f) + const { + std::string root_path_full = root_path; + if (root_path_full.back() != '/') { + root_path_full += '/'; + } + + void* cookie; + if (StartIteration(zip_handle_.get(), &cookie, root_path_full, "") != 0) { + return false; + } + + std::string name; + ::ZipEntry entry{}; + + // We need to hold back directories because many paths will contain them and we want to only + // surface one. + std::set<std::string> dirs{}; + + int32_t result; + while ((result = Next(cookie, &entry, &name)) == 0) { + StringPiece full_file_path(name); + StringPiece leaf_file_path = full_file_path.substr(root_path_full.size()); + + if (!leaf_file_path.empty()) { + auto iter = std::find(leaf_file_path.begin(), leaf_file_path.end(), '/'); + if (iter != leaf_file_path.end()) { + std::string dir = + leaf_file_path.substr(0, std::distance(leaf_file_path.begin(), iter)).to_string(); + dirs.insert(std::move(dir)); + } else { + f(leaf_file_path, kFileTypeRegular); + } + } + } + EndIteration(cookie); + + // Now present the unique directories. + for (const std::string& dir : dirs) { + f(dir, kFileTypeDirectory); + } + + // -1 is end of iteration, anything else is an error. + return result == -1; +} + +std::optional<uint32_t> ZipAssetsProvider::GetCrc(std::string_view path) const { + ::ZipEntry entry; + if (FindEntry(zip_handle_.get(), path, &entry) != 0) { + return {}; + } + return entry.crc32; +} + +const std::string& ZipAssetsProvider::GetDebugName() const { + return name_.GetDebugName(); +} + +bool ZipAssetsProvider::IsUpToDate() const { + struct stat sb{}; + if (fstat(GetFileDescriptor(zip_handle_.get()), &sb) < 0) { + // If fstat fails on the zip archive, return true so the zip archive the resource system does + // attempt to refresh the ApkAsset. + return true; + } + return last_mod_time_ == sb.st_mtime; +} + +DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, time_t last_mod_time) + : dir_(std::forward<std::string>(path)), last_mod_time_(last_mod_time) {} + +std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::string path) { + struct stat sb{}; + const int result = stat(path.c_str(), &sb); + if (result == -1) { + LOG(ERROR) << "Failed to find directory '" << path << "'."; + return nullptr; + } + + if (!S_ISDIR(sb.st_mode)) { + LOG(ERROR) << "Path '" << path << "' is not a directory."; + return nullptr; + } + + if (path[path.size() - 1] != OS_PATH_SEPARATOR) { + path += OS_PATH_SEPARATOR; + } + + return std::unique_ptr<DirectoryAssetsProvider>(new DirectoryAssetsProvider(std::move(path), + sb.st_mtime)); +} + +std::unique_ptr<Asset> DirectoryAssetsProvider::OpenInternal(const std::string& path, + Asset::AccessMode /* mode */, + bool* file_exists) const { + const std::string resolved_path = dir_ + path; + if (file_exists != nullptr) { + struct stat sb{}; + *file_exists = (stat(resolved_path.c_str(), &sb) != -1) && S_ISREG(sb.st_mode); + } + + return CreateAssetFromFile(resolved_path); +} + +bool DirectoryAssetsProvider::ForEachFile( + const std::string& /* root_path */, + const std::function<void(const StringPiece&, FileType)>& /* f */) + const { + return true; +} + +const std::string& DirectoryAssetsProvider::GetDebugName() const { + return dir_; +} + +bool DirectoryAssetsProvider::IsUpToDate() const { + struct stat sb{}; + if (stat(dir_.c_str(), &sb) < 0) { + // If stat fails on the zip archive, return true so the zip archive the resource system does + // attempt to refresh the ApkAsset. + return true; + } + return last_mod_time_ == sb.st_mtime; +} + +MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& primary, + std::unique_ptr<AssetsProvider>&& secondary) + : primary_(std::forward<std::unique_ptr<AssetsProvider>>(primary)), + secondary_(std::forward<std::unique_ptr<AssetsProvider>>(secondary)) { + if (primary_->GetDebugName() == kEmptyDebugString) { + debug_name_ = secondary_->GetDebugName(); + } else if (secondary_->GetDebugName() == kEmptyDebugString) { + debug_name_ = primary_->GetDebugName(); + } else { + debug_name_ = primary_->GetDebugName() + " and " + secondary_->GetDebugName(); + } +} + +std::unique_ptr<AssetsProvider> MultiAssetsProvider::Create( + std::unique_ptr<AssetsProvider>&& primary, std::unique_ptr<AssetsProvider>&& secondary) { + if (primary == nullptr || secondary == nullptr) { + return nullptr; + } + return std::unique_ptr<MultiAssetsProvider>(new MultiAssetsProvider(std::move(primary), + std::move(secondary))); +} + +std::unique_ptr<Asset> MultiAssetsProvider::OpenInternal(const std::string& path, + Asset::AccessMode mode, + bool* file_exists) const { + auto asset = primary_->Open(path, mode, file_exists); + return (asset) ? std::move(asset) : secondary_->Open(path, mode, file_exists); +} + +bool MultiAssetsProvider::ForEachFile(const std::string& root_path, + const std::function<void(const StringPiece&, FileType)>& f) + const { + return primary_->ForEachFile(root_path, f) && secondary_->ForEachFile(root_path, f); +} + +const std::string& MultiAssetsProvider::GetDebugName() const { + return debug_name_; +} + +bool MultiAssetsProvider::IsUpToDate() const { + return primary_->IsUpToDate() && secondary_->IsUpToDate(); +} + +std::unique_ptr<AssetsProvider> EmptyAssetsProvider::Create() { + return std::make_unique<EmptyAssetsProvider>(); +} + +std::unique_ptr<Asset> EmptyAssetsProvider::OpenInternal(const std::string& /* path */, + Asset::AccessMode /* mode */, + bool* file_exists) const { + if (file_exists) { + *file_exists = false; + } + return nullptr; +} + +bool EmptyAssetsProvider::ForEachFile( + const std::string& /* root_path */, + const std::function<void(const StringPiece&, FileType)>& /* f */) const { + return true; +} + +const std::string& EmptyAssetsProvider::GetDebugName() const { + const static std::string kEmpty = kEmptyDebugString; + return kEmpty; +} + +bool EmptyAssetsProvider::IsUpToDate() const { + return true; +} + +} // namespace android
\ No newline at end of file diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp index adb383f95d40..efd1f6a25786 100644 --- a/libs/androidfw/Idmap.cpp +++ b/libs/androidfw/Idmap.cpp @@ -54,12 +54,6 @@ struct Idmap_header { }; struct Idmap_data_header { - uint8_t target_package_id; - uint8_t overlay_package_id; - - // Padding to ensure 4 byte alignment for target_entry_count - uint16_t p0; - uint32_t target_entry_count; uint32_t target_inline_entry_count; uint32_t overlay_entry_count; @@ -158,19 +152,19 @@ IdmapResMap::Result IdmapResMap::Lookup(uint32_t target_res_id) const { return {}; } - // The resource ids encoded within the idmap are build-time resource ids. - target_res_id = (0x00FFFFFFU & target_res_id) - | (((uint32_t) data_header_->target_package_id) << 24U); + // The resource ids encoded within the idmap are build-time resource ids so do not consider the + // package id when determining if the resource in the target package is overlaid. + target_res_id &= 0x00FFFFFFU; // Check if the target resource is mapped to an overlay resource. auto first_entry = entries_; auto end_entry = entries_ + dtohl(data_header_->target_entry_count); auto entry = std::lower_bound(first_entry, end_entry, target_res_id, - [](const Idmap_target_entry &e, const uint32_t target_id) { - return dtohl(e.target_id) < target_id; + [](const Idmap_target_entry& e, const uint32_t target_id) { + return (0x00FFFFFFU & dtohl(e.target_id)) < target_id; }); - if (entry != end_entry && dtohl(entry->target_id) == target_res_id) { + if (entry != end_entry && (0x00FFFFFFU & dtohl(entry->target_id)) == target_res_id) { uint32_t overlay_resource_id = dtohl(entry->overlay_id); // Lookup the resource without rewriting the overlay resource id back to the target resource id // being looked up. @@ -182,12 +176,13 @@ IdmapResMap::Result IdmapResMap::Lookup(uint32_t target_res_id) const { auto first_inline_entry = inline_entries_; auto end_inline_entry = inline_entries_ + dtohl(data_header_->target_inline_entry_count); auto inline_entry = std::lower_bound(first_inline_entry, end_inline_entry, target_res_id, - [](const Idmap_target_entry_inline &e, + [](const Idmap_target_entry_inline& e, const uint32_t target_id) { - return dtohl(e.target_id) < target_id; + return (0x00FFFFFFU & dtohl(e.target_id)) < target_id; }); - if (inline_entry != end_inline_entry && dtohl(inline_entry->target_id) == target_res_id) { + if (inline_entry != end_inline_entry && + (0x00FFFFFFU & dtohl(inline_entry->target_id)) == target_res_id) { return Result(inline_entry->value); } return {}; @@ -235,7 +230,7 @@ std::optional<std::string_view> ReadString(const uint8_t** in_out_data_ptr, size } return std::string_view(data, *len); } -} +} // namespace LoadedIdmap::LoadedIdmap(std::string&& idmap_path, const Idmap_header* header, @@ -257,8 +252,8 @@ LoadedIdmap::LoadedIdmap(std::string&& idmap_path, target_apk_path_(target_apk_path), idmap_last_mod_time_(getFileModDate(idmap_path_.data())) {} -std::unique_ptr<const LoadedIdmap> LoadedIdmap::Load(const StringPiece& idmap_path, - const StringPiece& idmap_data) { +std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(const StringPiece& idmap_path, + const StringPiece& idmap_data) { ATRACE_CALL(); size_t data_size = idmap_data.size(); auto data_ptr = reinterpret_cast<const uint8_t*>(idmap_data.data()); diff --git a/libs/androidfw/LoadedArsc.cpp b/libs/androidfw/LoadedArsc.cpp index 996b42426282..2a70f0d6a804 100644 --- a/libs/androidfw/LoadedArsc.cpp +++ b/libs/androidfw/LoadedArsc.cpp @@ -767,10 +767,10 @@ bool LoadedArsc::LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap, return true; } -std::unique_ptr<const LoadedArsc> LoadedArsc::Load(incfs::map_ptr<void> data, - const size_t length, - const LoadedIdmap* loaded_idmap, - const package_property_t property_flags) { +std::unique_ptr<LoadedArsc> LoadedArsc::Load(incfs::map_ptr<void> data, + const size_t length, + const LoadedIdmap* loaded_idmap, + const package_property_t property_flags) { ATRACE_NAME("LoadedArsc::Load"); // Not using make_unique because the constructor is private. @@ -799,11 +799,10 @@ std::unique_ptr<const LoadedArsc> LoadedArsc::Load(incfs::map_ptr<void> data, } } - // Need to force a move for mingw32. - return std::move(loaded_arsc); + return loaded_arsc; } -std::unique_ptr<const LoadedArsc> LoadedArsc::CreateEmpty() { +std::unique_ptr<LoadedArsc> LoadedArsc::CreateEmpty() { return std::unique_ptr<LoadedArsc>(new LoadedArsc()); } diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index 223382731bc0..30500abc39c0 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -25,6 +25,7 @@ #include <string.h> #include <algorithm> +#include <fstream> #include <limits> #include <map> #include <memory> @@ -44,6 +45,7 @@ #ifdef __ANDROID__ #include <binder/TextOutput.h> + #endif #ifndef INT32_MAX @@ -233,6 +235,15 @@ void Res_png_9patch::serialize(const Res_png_9patch& patch, const int32_t* xDivs fill9patchOffsets(reinterpret_cast<Res_png_9patch*>(outData)); } +bool IsFabricatedOverlay(const std::string& path) { + std::ifstream fin(path); + uint32_t magic; + if (fin.read(reinterpret_cast<char*>(&magic), sizeof(uint32_t))) { + return magic == kFabricatedOverlayMagic; + } + return false; +} + static bool assertIdmapHeader(const void* idmap, size_t size) { if (reinterpret_cast<uintptr_t>(idmap) & 0x03) { ALOGE("idmap: header is not word aligned"); diff --git a/libs/androidfw/include/androidfw/ApkAssets.h b/libs/androidfw/include/androidfw/ApkAssets.h index e57490aab2d8..d0019ed6be30 100644 --- a/libs/androidfw/include/androidfw/ApkAssets.h +++ b/libs/androidfw/include/androidfw/ApkAssets.h @@ -24,104 +24,49 @@ #include "android-base/unique_fd.h" #include "androidfw/Asset.h" +#include "androidfw/AssetsProvider.h" #include "androidfw/Idmap.h" #include "androidfw/LoadedArsc.h" #include "androidfw/misc.h" -struct ZipArchive; -typedef ZipArchive* ZipArchiveHandle; - namespace android { -class LoadedIdmap; - -// Interface for retrieving assets provided by an ApkAssets. -class AssetsProvider { +// Holds an APK. +class ApkAssets { public: - virtual ~AssetsProvider() = default; - // Opens a file for reading. - std::unique_ptr<Asset> Open(const std::string& path, - Asset::AccessMode mode = Asset::AccessMode::ACCESS_RANDOM, - bool* file_exists = nullptr) const { - return OpenInternal(path, mode, file_exists); - } + // Creates an ApkAssets from a path on device. + static std::unique_ptr<ApkAssets> Load(const std::string& path, + package_property_t flags = 0U); - // Iterate over all files and directories provided by the zip. The order of iteration is stable. - virtual bool ForEachFile(const std::string& /* path */, - const std::function<void(const StringPiece&, FileType)>& /* f */) const { - return true; - } + // Creates an ApkAssets from an open file descriptor. + static std::unique_ptr<ApkAssets> LoadFromFd(base::unique_fd fd, + const std::string& debug_name, + package_property_t flags = 0U, + off64_t offset = 0, + off64_t len = AssetsProvider::kUnknownLength); - protected: - AssetsProvider() = default; + // Creates an ApkAssets from an AssetProvider. + // The ApkAssets will take care of destroying the AssetsProvider when it is destroyed. + static std::unique_ptr<ApkAssets> Load(std::unique_ptr<AssetsProvider> assets, + package_property_t flags = 0U); - virtual std::unique_ptr<Asset> OpenInternal(const std::string& path, - Asset::AccessMode mode, - bool* file_exists) const = 0; - - private: - DISALLOW_COPY_AND_ASSIGN(AssetsProvider); -}; - -class ZipAssetsProvider; - -// Holds an APK. -class ApkAssets { - public: - // This means the data extends to the end of the file. - static constexpr off64_t kUnknownLength = -1; - - // Creates an ApkAssets. - // If `system` is true, the package is marked as a system package, and allows some functions to - // filter out this package when computing what configurations/resources are available. - static std::unique_ptr<const ApkAssets> Load( - const std::string& path, package_property_t flags = 0U, - std::unique_ptr<const AssetsProvider> override_asset = nullptr); - - // Creates an ApkAssets from the given file descriptor, and takes ownership of the file - // descriptor. The `friendly_name` is some name that will be used to identify the source of - // this ApkAssets in log messages and other debug scenarios. - // If `length` equals kUnknownLength, offset must equal 0; otherwise, the apk data will be read - // using the `offset` into the file descriptor and will be `length` bytes long. - static std::unique_ptr<const ApkAssets> LoadFromFd( - base::unique_fd fd, const std::string& friendly_name, package_property_t flags = 0U, - std::unique_ptr<const AssetsProvider> override_asset = nullptr, off64_t offset = 0, - off64_t length = kUnknownLength); - - // Creates an ApkAssets from the given path which points to a resources.arsc. - static std::unique_ptr<const ApkAssets> LoadTable( - const std::string& path, package_property_t flags = 0U, - std::unique_ptr<const AssetsProvider> override_asset = nullptr); - - // Creates an ApkAssets from the given file descriptor which points to an resources.arsc, and - // takes ownership of the file descriptor. - // If `length` equals kUnknownLength, offset must equal 0; otherwise, the .arsc data will be read - // using the `offset` into the file descriptor and will be `length` bytes long. - static std::unique_ptr<const ApkAssets> LoadTableFromFd( - base::unique_fd fd, const std::string& friendly_name, package_property_t flags = 0U, - std::unique_ptr<const AssetsProvider> override_asset = nullptr, off64_t offset = 0, - off64_t length = kUnknownLength); + // Creates an ApkAssets from the given asset file representing a resources.arsc. + static std::unique_ptr<ApkAssets> LoadTable(std::unique_ptr<Asset> resources_asset, + std::unique_ptr<AssetsProvider> assets, + package_property_t flags = 0U); // Creates an ApkAssets from an IDMAP, which contains the original APK path, and the overlay // data. - static std::unique_ptr<const ApkAssets> LoadOverlay(const std::string& idmap_path, - package_property_t flags = 0U); - - // Creates an ApkAssets from the directory path. File-based resources are read within the - // directory as if the directory is an APK. - static std::unique_ptr<const ApkAssets> LoadFromDir( - const std::string& path, package_property_t flags = 0U, - std::unique_ptr<const AssetsProvider> override_asset = nullptr); - - // Creates a totally empty ApkAssets with no resources table and no file entries. - static std::unique_ptr<const ApkAssets> LoadEmpty( - package_property_t flags = 0U, - std::unique_ptr<const AssetsProvider> override_asset = nullptr); - - const std::string& GetPath() const { - return path_; - } + static std::unique_ptr<ApkAssets> LoadOverlay(const std::string& idmap_path, + package_property_t flags = 0U); + + // TODO(177101983): Remove all uses of GetPath for checking whether two ApkAssets are the same. + // With the introduction of ResourcesProviders, not all ApkAssets have paths. This could cause + // bugs when path is used for comparison because multiple ApkAssets could have the same "firendly + // name". Use pointer equality instead. ResourceManager caches and reuses ApkAssets so the + // same asset should have the same pointer. + const std::string& GetPath() const; const AssetsProvider* GetAssetsProvider() const { return assets_provider_.get(); @@ -146,53 +91,40 @@ class ApkAssets { // Returns whether the resources.arsc is allocated in RAM (not mmapped). bool IsTableAllocated() const { - return resources_asset_ && resources_asset_->isAllocated(); + return resources_asset_ != nullptr && resources_asset_->isAllocated(); } bool IsUpToDate() const; - // Creates an Asset from a file on disk. - static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path); - - // Creates an Asset from a file descriptor. - // - // The asset takes ownership of the file descriptor. If `length` equals kUnknownLength, offset - // must equal 0; otherwise, the asset data will be read using the `offset` into the file - // descriptor and will be `length` bytes long. - static std::unique_ptr<Asset> CreateAssetFromFd(base::unique_fd fd, - const char* path, - off64_t offset = 0, - off64_t length = kUnknownLength); private: - DISALLOW_COPY_AND_ASSIGN(ApkAssets); - - static std::unique_ptr<const ApkAssets> LoadImpl( - std::unique_ptr<const AssetsProvider> assets, const std::string& path, - package_property_t property_flags, - std::unique_ptr<const AssetsProvider> override_assets = nullptr, - std::unique_ptr<Asset> idmap_asset = nullptr, - std::unique_ptr<const LoadedIdmap> idmap = nullptr); - - static std::unique_ptr<const ApkAssets> LoadTableImpl( - std::unique_ptr<Asset> resources_asset, const std::string& path, - package_property_t property_flags, - std::unique_ptr<const AssetsProvider> override_assets = nullptr); - - ApkAssets(std::unique_ptr<const AssetsProvider> assets_provider, - std::string path, - time_t last_mod_time, - package_property_t property_flags); - - std::unique_ptr<const AssetsProvider> assets_provider_; - const std::string path_; - time_t last_mod_time_; - package_property_t property_flags_ = 0U; + static std::unique_ptr<ApkAssets> LoadImpl(std::unique_ptr<AssetsProvider> assets, + package_property_t property_flags, + std::unique_ptr<Asset> idmap_asset, + std::unique_ptr<LoadedIdmap> loaded_idmap); + + static std::unique_ptr<ApkAssets> LoadImpl(std::unique_ptr<Asset> resources_asset, + std::unique_ptr<AssetsProvider> assets, + package_property_t property_flags, + std::unique_ptr<Asset> idmap_asset, + std::unique_ptr<LoadedIdmap> loaded_idmap); + + ApkAssets(std::unique_ptr<Asset> resources_asset, + std::unique_ptr<LoadedArsc> loaded_arsc, + std::unique_ptr<AssetsProvider> assets, + package_property_t property_flags, + std::unique_ptr<Asset> idmap_asset, + std::unique_ptr<LoadedIdmap> loaded_idmap); + std::unique_ptr<Asset> resources_asset_; + std::unique_ptr<LoadedArsc> loaded_arsc_; + + std::unique_ptr<AssetsProvider> assets_provider_; + package_property_t property_flags_ = 0U; + std::unique_ptr<Asset> idmap_asset_; - std::unique_ptr<const LoadedArsc> loaded_arsc_; - std::unique_ptr<const LoadedIdmap> loaded_idmap_; + std::unique_ptr<LoadedIdmap> loaded_idmap_; }; -} // namespace android +} // namespace android -#endif /* APKASSETS_H_ */ +#endif // APKASSETS_H_
\ No newline at end of file diff --git a/libs/androidfw/include/androidfw/Asset.h b/libs/androidfw/include/androidfw/Asset.h index 80bae20f3419..40c91a6fcbf5 100644 --- a/libs/androidfw/include/androidfw/Asset.h +++ b/libs/androidfw/include/androidfw/Asset.h @@ -167,8 +167,8 @@ protected: private: /* AssetManager needs access to our "create" functions */ friend class AssetManager; - friend class ApkAssets; - friend class ZipAssetsProvider; + friend struct ZipAssetsProvider; + friend struct AssetsProvider; /* * Create the asset from a named file on disk. diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h index 6fbd6aa0df7b..2255973f1039 100644 --- a/libs/androidfw/include/androidfw/AssetManager2.h +++ b/libs/androidfw/include/androidfw/AssetManager2.h @@ -101,7 +101,7 @@ class AssetManager2 { // Only pass invalidate_caches=false when it is known that the structure // change in ApkAssets is due to a safe addition of resources with completely // new resource IDs. - bool SetApkAssets(const std::vector<const ApkAssets*>& apk_assets, bool invalidate_caches = true); + bool SetApkAssets(std::vector<const ApkAssets*> apk_assets, bool invalidate_caches = true); inline const std::vector<const ApkAssets*> GetApkAssets() const { return apk_assets_; diff --git a/libs/androidfw/include/androidfw/AssetsProvider.h b/libs/androidfw/include/androidfw/AssetsProvider.h new file mode 100644 index 000000000000..6f16ff453905 --- /dev/null +++ b/libs/androidfw/include/androidfw/AssetsProvider.h @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2021 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. + */ + +#ifndef ANDROIDFW_ASSETSPROVIDER_H +#define ANDROIDFW_ASSETSPROVIDER_H + +#include <memory> +#include <string> + +#include "android-base/macros.h" +#include "android-base/unique_fd.h" + +#include "androidfw/Asset.h" +#include "androidfw/Idmap.h" +#include "androidfw/LoadedArsc.h" +#include "androidfw/misc.h" + +struct ZipArchive; + +namespace android { + +// Interface responsible for opening and iterating through asset files. +struct AssetsProvider { + static constexpr off64_t kUnknownLength = -1; + + // Opens a file for reading. If `file_exists` is not null, it will be set to `true` if the file + // exists. This is useful for determining if the file exists but was unable to be opened due to + // an I/O error. + std::unique_ptr<Asset> Open(const std::string& path, + Asset::AccessMode mode = Asset::AccessMode::ACCESS_RANDOM, + bool* file_exists = nullptr) const; + + // Iterate over all files and directories provided by the interface. The order of iteration is + // stable. + virtual bool ForEachFile(const std::string& path, + const std::function<void(const StringPiece&, FileType)>& f) const = 0; + + // Retrieves a name that represents the interface. This may or may not be the path of the + // interface source. + WARN_UNUSED virtual const std::string& GetDebugName() const = 0; + + // Returns whether the interface provides the most recent version of its files. + WARN_UNUSED virtual bool IsUpToDate() const = 0; + + // Creates an Asset from a file on disk. + static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path); + + // Creates an Asset from a file descriptor. + // + // The asset takes ownership of the file descriptor. If `length` equals kUnknownLength, offset + // must equal 0; otherwise, the asset data will be read using the `offset` into the file + // descriptor and will be `length` bytes long. + static std::unique_ptr<Asset> CreateAssetFromFd(base::unique_fd fd, + const char* path, + off64_t offset = 0, + off64_t length = AssetsProvider::kUnknownLength); + + virtual ~AssetsProvider() = default; + protected: + virtual std::unique_ptr<Asset> OpenInternal(const std::string& path, Asset::AccessMode mode, + bool* file_exists) const = 0; +}; + +// Supplies assets from a zip archive. +struct ZipAssetsProvider : public AssetsProvider { + static std::unique_ptr<ZipAssetsProvider> Create(std::string path); + static std::unique_ptr<ZipAssetsProvider> Create(base::unique_fd fd, + std::string friendly_name, + off64_t offset = 0, + off64_t len = kUnknownLength); + + bool ForEachFile(const std::string& root_path, + const std::function<void(const StringPiece&, FileType)>& f) const override; + + WARN_UNUSED const std::string& GetDebugName() const override; + WARN_UNUSED bool IsUpToDate() const override; + + WARN_UNUSED std::optional<uint32_t> GetCrc(std::string_view path) const; + + ~ZipAssetsProvider() override = default; + protected: + std::unique_ptr<Asset> OpenInternal(const std::string& path, Asset::AccessMode mode, + bool* file_exists) const override; + + private: + struct PathOrDebugName; + ZipAssetsProvider(ZipArchive* handle, PathOrDebugName&& path, time_t last_mod_time); + + struct PathOrDebugName { + PathOrDebugName(std::string&& value, bool is_path); + + // Retrieves the path or null if this class represents a debug name. + WARN_UNUSED const std::string* GetPath() const; + + // Retrieves a name that represents the interface. This may or may not represent a path. + WARN_UNUSED const std::string& GetDebugName() const; + + private: + std::string value_; + bool is_path_; + }; + + std::unique_ptr<ZipArchive, void (*)(ZipArchive*)> zip_handle_; + PathOrDebugName name_; + time_t last_mod_time_; +}; + +// Supplies assets from a root directory. +struct DirectoryAssetsProvider : public AssetsProvider { + static std::unique_ptr<DirectoryAssetsProvider> Create(std::string root_dir); + + bool ForEachFile(const std::string& path, + const std::function<void(const StringPiece&, FileType)>& f) const override; + + WARN_UNUSED const std::string& GetDebugName() const override; + WARN_UNUSED bool IsUpToDate() const override; + + ~DirectoryAssetsProvider() override = default; + protected: + std::unique_ptr<Asset> OpenInternal(const std::string& path, + Asset::AccessMode mode, + bool* file_exists) const override; + + private: + explicit DirectoryAssetsProvider(std::string&& path, time_t last_mod_time); + std::string dir_; + time_t last_mod_time_; +}; + +// Supplies assets from a `primary` asset provider and falls back to supplying assets from the +// `secondary` asset provider if the asset cannot be found in the `primary`. +struct MultiAssetsProvider : public AssetsProvider { + static std::unique_ptr<AssetsProvider> Create(std::unique_ptr<AssetsProvider>&& primary, + std::unique_ptr<AssetsProvider>&& secondary); + + bool ForEachFile(const std::string& root_path, + const std::function<void(const StringPiece&, FileType)>& f) const override; + + WARN_UNUSED const std::string& GetDebugName() const override; + WARN_UNUSED bool IsUpToDate() const override; + + ~MultiAssetsProvider() override = default; + protected: + std::unique_ptr<Asset> OpenInternal( + const std::string& path, Asset::AccessMode mode, bool* file_exists) const override; + + private: + MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& primary, + std::unique_ptr<AssetsProvider>&& secondary); + + std::unique_ptr<AssetsProvider> primary_; + std::unique_ptr<AssetsProvider> secondary_; + std::string debug_name_; +}; + +// Does not provide any assets. +struct EmptyAssetsProvider : public AssetsProvider { + static std::unique_ptr<AssetsProvider> Create(); + + bool ForEachFile(const std::string& path, + const std::function<void(const StringPiece&, FileType)>& f) const override; + + WARN_UNUSED const std::string& GetDebugName() const override; + WARN_UNUSED bool IsUpToDate() const override; + + ~EmptyAssetsProvider() override = default; + protected: + std::unique_ptr<Asset> OpenInternal(const std::string& path, Asset::AccessMode mode, + bool* file_exists) const override; +}; + +} // namespace android + +#endif /* ANDROIDFW_ASSETSPROVIDER_H */ diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index fd9a8d13e0c6..6804472b3d17 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -149,8 +149,8 @@ class IdmapResMap { class LoadedIdmap { public: // Loads an IDMAP from a chunk of memory. Returns nullptr if the IDMAP data was malformed. - static std::unique_ptr<const LoadedIdmap> Load(const StringPiece& idmap_path, - const StringPiece& idmap_data); + static std::unique_ptr<LoadedIdmap> Load(const StringPiece& idmap_path, + const StringPiece& idmap_data); // Returns the path to the IDMAP. std::string_view IdmapPath() const { @@ -168,15 +168,14 @@ class LoadedIdmap { } // Returns a mapping from target resource ids to overlay values. - const IdmapResMap GetTargetResourcesMap( - uint8_t target_assigned_package_id, const OverlayDynamicRefTable* overlay_ref_table) const { + const IdmapResMap GetTargetResourcesMap(uint8_t target_assigned_package_id, + const OverlayDynamicRefTable* overlay_ref_table) const { return IdmapResMap(data_header_, target_entries_, target_inline_entries_, target_assigned_package_id, overlay_ref_table); } // Returns a dynamic reference table for a loaded overlay package. - const OverlayDynamicRefTable GetOverlayDynamicRefTable( - uint8_t target_assigned_package_id) const { + const OverlayDynamicRefTable GetOverlayDynamicRefTable(uint8_t target_assigned_package_id) const { return OverlayDynamicRefTable(data_header_, overlay_entries_, target_assigned_package_id); } diff --git a/libs/androidfw/include/androidfw/LoadedArsc.h b/libs/androidfw/include/androidfw/LoadedArsc.h index 891fb90d8eeb..d9225cd812ef 100644 --- a/libs/androidfw/include/androidfw/LoadedArsc.h +++ b/libs/androidfw/include/androidfw/LoadedArsc.h @@ -300,17 +300,14 @@ class LoadedArsc { public: // Load a resource table from memory pointed to by `data` of size `len`. // The lifetime of `data` must out-live the LoadedArsc returned from this method. - // If `system` is set to true, the LoadedArsc is considered as a system provided resource. - // If `load_as_shared_library` is set to true, the application package (0x7f) is treated - // as a shared library (0x00). When loaded into an AssetManager, the package will be assigned an - // ID. - static std::unique_ptr<const LoadedArsc> Load(incfs::map_ptr<void> data, - size_t length, - const LoadedIdmap* loaded_idmap = nullptr, - package_property_t property_flags = 0U); + + static std::unique_ptr<LoadedArsc> Load(incfs::map_ptr<void> data, + size_t length, + const LoadedIdmap* loaded_idmap = nullptr, + package_property_t property_flags = 0U); // Create an empty LoadedArsc. This is used when an APK has no resources.arsc. - static std::unique_ptr<const LoadedArsc> CreateEmpty(); + static std::unique_ptr<LoadedArsc> CreateEmpty(); // Returns the string pool where all string resource values // (Res_value::dataType == Res_value::TYPE_STRING) are indexed. diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index bfd564c258ee..168a863df2bc 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -43,8 +43,19 @@ namespace android { -constexpr const static uint32_t kIdmapMagic = 0x504D4449u; -constexpr const static uint32_t kIdmapCurrentVersion = 0x00000007u; +constexpr const uint32_t kIdmapMagic = 0x504D4449u; +constexpr const uint32_t kIdmapCurrentVersion = 0x00000008u; + +// This must never change. +constexpr const uint32_t kFabricatedOverlayMagic = 0x4f525246; // FRRO (big endian) + +// The version should only be changed when a backwards-incompatible change must be made to the +// fabricated overlay file format. Old fabricated overlays must be migrated to the new file format +// to prevent losing fabricated overlay data. +constexpr const uint32_t kFabricatedOverlayCurrentVersion = 1; + +// Returns whether or not the path represents a fabricated overlay. +bool IsFabricatedOverlay(const std::string& path); /** * In C++11, char16_t is defined as *at least* 16 bits. We do a lot of diff --git a/libs/androidfw/tests/Idmap_test.cpp b/libs/androidfw/tests/Idmap_test.cpp index 3f0c7cbc8ffc..b43491548e2b 100644 --- a/libs/androidfw/tests/Idmap_test.cpp +++ b/libs/androidfw/tests/Idmap_test.cpp @@ -27,6 +27,8 @@ #include "data/overlayable/R.h" #include "data/system/R.h" +using ::testing::NotNull; + namespace overlay = com::android::overlay; namespace overlayable = com::android::overlayable; @@ -195,7 +197,11 @@ TEST_F(IdmapTest, OverlaidResourceHasSameName) { } TEST_F(IdmapTest, OverlayLoaderInterop) { - auto loader_assets = ApkAssets::LoadTable("loader/resources.arsc", PROPERTY_LOADER); + auto asset = AssetsProvider::CreateAssetFromFile(GetTestDataPath() + "/loader/resources.arsc"); + ASSERT_THAT(asset, NotNull()); + + auto loader_assets = ApkAssets::LoadTable(std::move(asset), EmptyAssetsProvider::Create(), + PROPERTY_LOADER); AssetManager2 asset_manager; asset_manager.SetApkAssets({overlayable_assets_.get(), loader_assets.get(), overlay_assets_.get()}); diff --git a/libs/androidfw/tests/LoadedArsc_test.cpp b/libs/androidfw/tests/LoadedArsc_test.cpp index 9aa363495131..f356c8130080 100644 --- a/libs/androidfw/tests/LoadedArsc_test.cpp +++ b/libs/androidfw/tests/LoadedArsc_test.cpp @@ -339,10 +339,8 @@ TEST(LoadedArscTest, GetOverlayableMap) { } TEST(LoadedArscTest, LoadCustomLoader) { - std::string contents; - - std::unique_ptr<Asset> - asset = ApkAssets::CreateAssetFromFile(GetTestDataPath() + "/loader/resources.arsc"); + auto asset = AssetsProvider::CreateAssetFromFile(GetTestDataPath() + "/loader/resources.arsc"); + ASSERT_THAT(asset, NotNull()); const StringPiece data( reinterpret_cast<const char*>(asset->getBuffer(true /*wordAligned*/)), diff --git a/libs/androidfw/tests/data/overlay/overlay.idmap b/libs/androidfw/tests/data/overlay/overlay.idmap Binary files differindex 723413c3cea8..88eadccb38cf 100644 --- a/libs/androidfw/tests/data/overlay/overlay.idmap +++ b/libs/androidfw/tests/data/overlay/overlay.idmap diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 615bf4de9fcc..f48122858267 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -335,7 +335,6 @@ cc_defaults { "jni/YuvToJpegEncoder.cpp", "jni/fonts/Font.cpp", "jni/fonts/FontFamily.cpp", - "jni/fonts/NativeFont.cpp", "jni/text/LineBreaker.cpp", "jni/text/MeasuredText.cpp", "jni/text/TextShaper.cpp", @@ -435,6 +434,7 @@ cc_defaults { "canvas/CanvasFrontend.cpp", "canvas/CanvasOpBuffer.cpp", "canvas/CanvasOpRasterizer.cpp", + "effects/StretchEffect.cpp", "pipeline/skia/SkiaDisplayList.cpp", "pipeline/skia/SkiaRecordingCanvas.cpp", "pipeline/skia/RenderNodeDrawable.cpp", diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h index 27be62269959..d5fee3f667a9 100644 --- a/libs/hwui/DeviceInfo.h +++ b/libs/hwui/DeviceInfo.h @@ -35,7 +35,6 @@ class DeviceInfo { public: static DeviceInfo* get(); - static float getMaxRefreshRate() { return get()->mMaxRefreshRate; } static int32_t getWidth() { return get()->mWidth; } static int32_t getHeight() { return get()->mHeight; } // Gets the density in density-independent pixels @@ -45,7 +44,6 @@ public: static int64_t getAppOffset() { return get()->mAppVsyncOffsetNanos; } // Sets the density in density-independent pixels static void setDensity(float density) { sDensity.store(density); } - static void setMaxRefreshRate(float refreshRate) { get()->mMaxRefreshRate = refreshRate; } static void setWidth(int32_t width) { get()->mWidth = width; } static void setHeight(int32_t height) { get()->mHeight = height; } static void setRefreshRate(float refreshRate) { @@ -91,7 +89,6 @@ private: SkColorType mWideColorType = SkColorType::kN32_SkColorType; int mDisplaysSize = 0; int mPhysicalDisplayIndex = -1; - float mMaxRefreshRate = 60.0; int32_t mWidth = 1080; int32_t mHeight = 1920; int64_t mVsyncPeriod = 16666666; diff --git a/libs/hwui/Layer.cpp b/libs/hwui/Layer.cpp index ca2ada9e8141..b14ade97ca5f 100644 --- a/libs/hwui/Layer.cpp +++ b/libs/hwui/Layer.cpp @@ -54,7 +54,7 @@ SkBlendMode Layer::getMode() const { } static inline SkScalar isIntegerAligned(SkScalar x) { - return fabsf(roundf(x) - x) <= NON_ZERO_EPSILON; + return MathUtils::isZero(roundf(x) - x); } // Disable filtering when there is no scaling in screen coordinates and the corners have the same diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index 65f4e8c8ecec..971a53a8b2dc 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -79,7 +79,6 @@ bool Properties::debuggingEnabled = false; bool Properties::isolatedProcess = false; int Properties::contextPriority = 0; -int Properties::defaultRenderAhead = -1; float Properties::defaultSdrWhitePoint = 200.f; bool Properties::load() { @@ -129,9 +128,6 @@ bool Properties::load() { runningInEmulator = base::GetBoolProperty(PROPERTY_QEMU_KERNEL, false); - defaultRenderAhead = std::max(-1, std::min(2, base::GetIntProperty(PROPERTY_RENDERAHEAD, - render_ahead().value_or(0)))); - return (prevDebugLayersUpdates != debugLayersUpdates) || (prevDebugOverdraw != debugOverdraw); } diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index 1639143ef87c..dcb79babad24 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -162,8 +162,6 @@ enum DebugLevel { */ #define PROPERTY_QEMU_KERNEL "ro.kernel.qemu" -#define PROPERTY_RENDERAHEAD "debug.hwui.render_ahead" - /////////////////////////////////////////////////////////////////////////////// // Misc /////////////////////////////////////////////////////////////////////////////// @@ -247,8 +245,6 @@ public: static int contextPriority; - static int defaultRenderAhead; - static float defaultSdrWhitePoint; private: diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp index 96118aaec29b..64b8b711f0a8 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -561,7 +561,7 @@ public: return; } c->concat(invertedMatrix); - mLayerSurface->draw(c, deviceBounds.fLeft, deviceBounds.fTop, nullptr); + mLayerSurface->draw(c, deviceBounds.fLeft, deviceBounds.fTop); } else { c->drawDrawable(drawable.get()); } diff --git a/libs/hwui/RenderProperties.h b/libs/hwui/RenderProperties.h index aeb60e6ce355..609706e2e49d 100644 --- a/libs/hwui/RenderProperties.h +++ b/libs/hwui/RenderProperties.h @@ -23,6 +23,7 @@ #include "Outline.h" #include "Rect.h" #include "RevealClip.h" +#include "effects/StretchEffect.h" #include "utils/MathUtils.h" #include "utils/PaintUtils.h" @@ -98,6 +99,10 @@ public: SkImageFilter* getImageFilter() const { return mImageFilter.get(); } + const StretchEffect& getStretchEffect() const { return mStretchEffect; } + + StretchEffect& mutableStretchEffect() { return mStretchEffect; } + // Sets alpha, xfermode, and colorfilter from an SkPaint // paint may be NULL, in which case defaults will be set bool setFromPaint(const SkPaint* paint); @@ -124,6 +129,7 @@ private: SkBlendMode mMode; sk_sp<SkColorFilter> mColorFilter; sk_sp<SkImageFilter> mImageFilter; + StretchEffect mStretchEffect; }; /* diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp index 0fad2d58cc8a..e1f5abd786bf 100644 --- a/libs/hwui/apex/jni_runtime.cpp +++ b/libs/hwui/apex/jni_runtime.cpp @@ -69,7 +69,6 @@ extern int register_android_graphics_drawable_AnimatedVectorDrawable(JNIEnv* env extern int register_android_graphics_drawable_VectorDrawable(JNIEnv* env); extern int register_android_graphics_fonts_Font(JNIEnv* env); extern int register_android_graphics_fonts_FontFamily(JNIEnv* env); -extern int register_android_graphics_fonts_NativeFont(JNIEnv* env); extern int register_android_graphics_pdf_PdfDocument(JNIEnv* env); extern int register_android_graphics_pdf_PdfEditor(JNIEnv* env); extern int register_android_graphics_pdf_PdfRenderer(JNIEnv* env); @@ -136,7 +135,6 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_graphics_drawable_VectorDrawable), REG_JNI(register_android_graphics_fonts_Font), REG_JNI(register_android_graphics_fonts_FontFamily), - REG_JNI(register_android_graphics_fonts_NativeFont), REG_JNI(register_android_graphics_pdf_PdfDocument), REG_JNI(register_android_graphics_pdf_PdfEditor), REG_JNI(register_android_graphics_pdf_PdfRenderer), diff --git a/libs/hwui/effects/StretchEffect.cpp b/libs/hwui/effects/StretchEffect.cpp new file mode 100644 index 000000000000..51cbc7592861 --- /dev/null +++ b/libs/hwui/effects/StretchEffect.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 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. + */ + +#include "StretchEffect.h" + +namespace android::uirenderer { + +sk_sp<SkImageFilter> StretchEffect::getImageFilter() const { + // TODO: Implement & Cache + // Probably need to use mutable to achieve caching + return nullptr; +} + +} // namespace android::uirenderer
\ No newline at end of file diff --git a/libs/hwui/effects/StretchEffect.h b/libs/hwui/effects/StretchEffect.h new file mode 100644 index 000000000000..7dfd6398765a --- /dev/null +++ b/libs/hwui/effects/StretchEffect.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 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. + */ + +#pragma once + +#include "utils/MathUtils.h" + +#include <SkPoint.h> +#include <SkRect.h> +#include <SkImageFilter.h> + +namespace android::uirenderer { + +// TODO: Inherit from base RenderEffect type? +class StretchEffect { +public: + enum class StretchInterpolator { + SmoothStep, + }; + + bool isEmpty() const { + return MathUtils::isZero(stretchDirection.x()) + && MathUtils::isZero(stretchDirection.y()); + } + + void setEmpty() { + *this = StretchEffect{}; + } + + void mergeWith(const StretchEffect& other) { + if (other.isEmpty()) { + return; + } + if (isEmpty()) { + *this = other; + return; + } + stretchDirection += other.stretchDirection; + if (isEmpty()) { + return setEmpty(); + } + stretchArea.join(other.stretchArea); + maxStretchAmount = std::max(maxStretchAmount, other.maxStretchAmount); + } + + sk_sp<SkImageFilter> getImageFilter() const; + + SkRect stretchArea {0, 0, 0, 0}; + SkVector stretchDirection {0, 0}; + float maxStretchAmount = 0; +}; + +} // namespace android::uirenderer diff --git a/libs/hwui/hwui/MinikinSkia.cpp b/libs/hwui/hwui/MinikinSkia.cpp index 0e338f35b8e7..2db3ace1cd43 100644 --- a/libs/hwui/hwui/MinikinSkia.cpp +++ b/libs/hwui/hwui/MinikinSkia.cpp @@ -30,10 +30,11 @@ namespace android { -MinikinFontSkia::MinikinFontSkia(sk_sp<SkTypeface> typeface, const void* fontData, size_t fontSize, - std::string_view filePath, int ttcIndex, +MinikinFontSkia::MinikinFontSkia(sk_sp<SkTypeface> typeface, int sourceId, const void* fontData, + size_t fontSize, std::string_view filePath, int ttcIndex, const std::vector<minikin::FontVariation>& axes) : mTypeface(std::move(typeface)) + , mSourceId(sourceId) , mFontData(fontData) , mFontSize(fontSize) , mTtcIndex(ttcIndex) @@ -141,8 +142,8 @@ std::shared_ptr<minikin::MinikinFont> MinikinFontSkia::createFontWithVariation( sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault()); sk_sp<SkTypeface> face(fm->makeFromStream(std::move(stream), args)); - return std::make_shared<MinikinFontSkia>(std::move(face), mFontData, mFontSize, mFilePath, - ttcIndex, variations); + return std::make_shared<MinikinFontSkia>(std::move(face), mSourceId, mFontData, mFontSize, + mFilePath, ttcIndex, variations); } // hinting<<16 | edging<<8 | bools:5bits diff --git a/libs/hwui/hwui/MinikinSkia.h b/libs/hwui/hwui/MinikinSkia.h index 77a21428f36a..de9a5c2af0aa 100644 --- a/libs/hwui/hwui/MinikinSkia.h +++ b/libs/hwui/hwui/MinikinSkia.h @@ -30,7 +30,7 @@ namespace android { class ANDROID_API MinikinFontSkia : public minikin::MinikinFont { public: - MinikinFontSkia(sk_sp<SkTypeface> typeface, const void* fontData, size_t fontSize, + MinikinFontSkia(sk_sp<SkTypeface> typeface, int sourceId, const void* fontData, size_t fontSize, std::string_view filePath, int ttcIndex, const std::vector<minikin::FontVariation>& axes); @@ -62,6 +62,7 @@ public: const std::vector<minikin::FontVariation>& GetAxes() const; std::shared_ptr<minikin::MinikinFont> createFontWithVariation( const std::vector<minikin::FontVariation>&) const; + int GetSourceId() const override { return mSourceId; } static uint32_t packFontFlags(const SkFont&); static void unpackFontFlags(SkFont*, uint32_t fontFlags); @@ -73,6 +74,7 @@ public: private: sk_sp<SkTypeface> mTypeface; + int mSourceId; // A raw pointer to the font data - it should be owned by some other object with // lifetime at least as long as this object. const void* mFontData; diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp index 03f1d62625f1..5a9d2508230e 100644 --- a/libs/hwui/hwui/Typeface.cpp +++ b/libs/hwui/hwui/Typeface.cpp @@ -185,9 +185,9 @@ void Typeface::setRobotoTypefaceForTest() { sk_sp<SkTypeface> typeface = SkTypeface::MakeFromStream(std::move(fontData)); LOG_ALWAYS_FATAL_IF(typeface == nullptr, "Failed to make typeface from %s", kRobotoFont); - std::shared_ptr<minikin::MinikinFont> font = std::make_shared<MinikinFontSkia>( - std::move(typeface), data, st.st_size, kRobotoFont, 0, - std::vector<minikin::FontVariation>()); + std::shared_ptr<minikin::MinikinFont> font = + std::make_shared<MinikinFontSkia>(std::move(typeface), 0, data, st.st_size, kRobotoFont, + 0, std::vector<minikin::FontVariation>()); std::vector<std::shared_ptr<minikin::Font>> fonts; fonts.push_back(minikin::Font::Builder(font).build()); diff --git a/libs/hwui/jni/FontFamily.cpp b/libs/hwui/jni/FontFamily.cpp index 2e85840cad99..ce5ac382aeff 100644 --- a/libs/hwui/jni/FontFamily.cpp +++ b/libs/hwui/jni/FontFamily.cpp @@ -17,15 +17,16 @@ #undef LOG_TAG #define LOG_TAG "Minikin" +#include <nativehelper/ScopedPrimitiveArray.h> +#include <nativehelper/ScopedUtfChars.h> +#include "FontUtils.h" +#include "GraphicsJNI.h" #include "SkData.h" #include "SkFontMgr.h" #include "SkRefCnt.h" #include "SkTypeface.h" -#include "GraphicsJNI.h" -#include <nativehelper/ScopedPrimitiveArray.h> -#include <nativehelper/ScopedUtfChars.h> #include "Utils.h" -#include "FontUtils.h" +#include "fonts/Font.h" #include <hwui/MinikinSkia.h> #include <hwui/Typeface.h> @@ -35,6 +36,12 @@ #include <memory> +/////////////////////////////////////////////////////////////////////////////////////////////////// +// +// The following JNI methods are kept only for compatibility reasons due to hidden API accesses. +// +/////////////////////////////////////////////////////////////////////////////////////////////////// + namespace android { struct NativeFamilyBuilder { @@ -125,8 +132,8 @@ static bool addSkTypeface(NativeFamilyBuilder* builder, sk_sp<SkData>&& data, in return false; } std::shared_ptr<minikin::MinikinFont> minikinFont = - std::make_shared<MinikinFontSkia>(std::move(face), fontPtr, fontSize, "", ttcIndex, - builder->axes); + std::make_shared<MinikinFontSkia>(std::move(face), fonts::getNewSourceId(), fontPtr, + fontSize, "", ttcIndex, builder->axes); minikin::Font::Builder fontBuilder(minikinFont); if (weight != RESOLVE_BY_FONT_TABLE) { diff --git a/libs/hwui/jni/RenderEffect.cpp b/libs/hwui/jni/RenderEffect.cpp index 97c40d695f97..a48d7f734e29 100644 --- a/libs/hwui/jni/RenderEffect.cpp +++ b/libs/hwui/jni/RenderEffect.cpp @@ -64,8 +64,8 @@ static jlong createBitmapEffect( sk_sp<SkImage> image = android::bitmap::toBitmap(bitmapHandle).makeImage(); SkRect srcRect = SkRect::MakeLTRB(srcLeft, srcTop, srcRight, srcBottom); SkRect dstRect = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); - sk_sp<SkImageFilter> bitmapFilter = - SkImageFilters::Image(image, srcRect, dstRect, kLow_SkFilterQuality); + sk_sp<SkImageFilter> bitmapFilter = SkImageFilters::Image( + image, srcRect, dstRect, SkSamplingOptions(SkFilterMode::kLinear)); return reinterpret_cast<jlong>(bitmapFilter.release()); } @@ -115,6 +115,18 @@ static jlong createChainEffect( return reinterpret_cast<jlong>(composeFilter.release()); } +static jlong createShaderEffect( + JNIEnv* env, + jobject, + jlong shaderHandle +) { + auto* shader = reinterpret_cast<const SkShader*>(shaderHandle); + sk_sp<SkImageFilter> shaderFilter = SkImageFilters::Shader( + sk_ref_sp(shader), nullptr + ); + return reinterpret_cast<jlong>(shaderFilter.release()); +} + static void RenderEffect_safeUnref(SkImageFilter* filter) { SkSafeUnref(filter); } @@ -130,11 +142,12 @@ static const JNINativeMethod gRenderEffectMethods[] = { {"nativeCreateBitmapEffect", "(JFFFFFFFF)J", (void*)createBitmapEffect}, {"nativeCreateColorFilterEffect", "(JJ)J", (void*)createColorFilterEffect}, {"nativeCreateBlendModeEffect", "(JJI)J", (void*)createBlendModeEffect}, - {"nativeCreateChainEffect", "(JJ)J", (void*)createChainEffect} + {"nativeCreateChainEffect", "(JJ)J", (void*)createChainEffect}, + {"nativeCreateShaderEffect", "(J)J", (void*)createShaderEffect} }; int register_android_graphics_RenderEffect(JNIEnv* env) { android::RegisterMethodsOrDie(env, "android/graphics/RenderEffect", gRenderEffectMethods, NELEM(gRenderEffectMethods)); return 0; -}
\ No newline at end of file +} diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp index 1dc5cd99eed1..2e4d7f62f671 100644 --- a/libs/hwui/jni/Shader.cpp +++ b/libs/hwui/jni/Shader.cpp @@ -239,14 +239,12 @@ static jlong ComposeShader_create(JNIEnv* env, jobject o, jlong matrixPtr, static jlong RuntimeShader_createShaderBuilder(JNIEnv* env, jobject, jstring sksl) { ScopedUtfChars strSksl(env, sksl); - auto result = SkRuntimeEffect::Make(SkString(strSksl.c_str())); - sk_sp<SkRuntimeEffect> effect = std::get<0>(result); - if (effect.get() == nullptr) { - const auto& err = std::get<1>(result); - doThrowIAE(env, err.c_str()); + auto result = SkRuntimeEffect::Make(SkString(strSksl.c_str()), SkRuntimeEffect::Options{}); + if (result.effect.get() == nullptr) { + doThrowIAE(env, result.errorText.c_str()); return 0; } - return reinterpret_cast<jlong>(new SkRuntimeShaderBuilder(std::move(effect))); + return reinterpret_cast<jlong>(new SkRuntimeShaderBuilder(std::move(result.effect))); } static void SkRuntimeShaderBuilder_delete(SkRuntimeShaderBuilder* builder) { diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp index 8f455fe4ab43..251323d34422 100644 --- a/libs/hwui/jni/Typeface.cpp +++ b/libs/hwui/jni/Typeface.cpp @@ -355,29 +355,48 @@ static void Typeface_forceSetStaticFinalField(JNIEnv *env, jclass cls, jstring f env->SetStaticObjectField(cls, fid, typeface); } +// Critical Native +static jint Typeface_getFamilySize(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { + return toTypeface(faceHandle)->fFontCollection->getFamilies().size(); +} + +// Critical Native +static jlong Typeface_getFamily(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle, jint index) { + std::shared_ptr<minikin::FontFamily> family = + toTypeface(faceHandle)->fFontCollection->getFamilies()[index]; + return reinterpret_cast<jlong>(new FontFamilyWrapper(std::move(family))); +} + +// Regular JNI +static void Typeface_warmUpCache(JNIEnv* env, jobject, jstring jFilePath) { + ScopedUtfChars filePath(env, jFilePath); + makeSkDataCached(filePath.c_str(), false /* fs verity */); +} /////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gTypefaceMethods[] = { - { "nativeCreateFromTypeface", "(JI)J", (void*)Typeface_createFromTypeface }, - { "nativeCreateFromTypefaceWithExactStyle", "(JIZ)J", - (void*)Typeface_createFromTypefaceWithExactStyle }, - { "nativeCreateFromTypefaceWithVariation", "(JLjava/util/List;)J", - (void*)Typeface_createFromTypefaceWithVariation }, - { "nativeCreateWeightAlias", "(JI)J", (void*)Typeface_createWeightAlias }, - { "nativeGetReleaseFunc", "()J", (void*)Typeface_getReleaseFunc }, - { "nativeGetStyle", "(J)I", (void*)Typeface_getStyle }, - { "nativeGetWeight", "(J)I", (void*)Typeface_getWeight }, - { "nativeCreateFromArray", "([JJII)J", - (void*)Typeface_createFromArray }, - { "nativeSetDefault", "(J)V", (void*)Typeface_setDefault }, - { "nativeGetSupportedAxes", "(J)[I", (void*)Typeface_getSupportedAxes }, - { "nativeRegisterGenericFamily", "(Ljava/lang/String;J)V", - (void*)Typeface_registerGenericFamily }, - { "nativeWriteTypefaces", "(Ljava/nio/ByteBuffer;[J)I", (void*)Typeface_writeTypefaces}, - { "nativeReadTypefaces", "(Ljava/nio/ByteBuffer;)[J", (void*)Typeface_readTypefaces}, - { "nativeForceSetStaticFinalField", "(Ljava/lang/String;Landroid/graphics/Typeface;)V", - (void*)Typeface_forceSetStaticFinalField }, + {"nativeCreateFromTypeface", "(JI)J", (void*)Typeface_createFromTypeface}, + {"nativeCreateFromTypefaceWithExactStyle", "(JIZ)J", + (void*)Typeface_createFromTypefaceWithExactStyle}, + {"nativeCreateFromTypefaceWithVariation", "(JLjava/util/List;)J", + (void*)Typeface_createFromTypefaceWithVariation}, + {"nativeCreateWeightAlias", "(JI)J", (void*)Typeface_createWeightAlias}, + {"nativeGetReleaseFunc", "()J", (void*)Typeface_getReleaseFunc}, + {"nativeGetStyle", "(J)I", (void*)Typeface_getStyle}, + {"nativeGetWeight", "(J)I", (void*)Typeface_getWeight}, + {"nativeCreateFromArray", "([JJII)J", (void*)Typeface_createFromArray}, + {"nativeSetDefault", "(J)V", (void*)Typeface_setDefault}, + {"nativeGetSupportedAxes", "(J)[I", (void*)Typeface_getSupportedAxes}, + {"nativeRegisterGenericFamily", "(Ljava/lang/String;J)V", + (void*)Typeface_registerGenericFamily}, + {"nativeWriteTypefaces", "(Ljava/nio/ByteBuffer;[J)I", (void*)Typeface_writeTypefaces}, + {"nativeReadTypefaces", "(Ljava/nio/ByteBuffer;)[J", (void*)Typeface_readTypefaces}, + {"nativeForceSetStaticFinalField", "(Ljava/lang/String;Landroid/graphics/Typeface;)V", + (void*)Typeface_forceSetStaticFinalField}, + {"nativeGetFamilySize", "(J)I", (void*)Typeface_getFamilySize}, + {"nativeGetFamily", "(JI)J", (void*)Typeface_getFamily}, + {"nativeWarmUpCache", "(Ljava/lang/String;)V", (void*)Typeface_warmUpCache}, }; int register_android_graphics_Typeface(JNIEnv* env) diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp index a146b64e29cc..4966bfa1c1e9 100644 --- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp +++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp @@ -603,14 +603,12 @@ static void android_view_ThreadedRenderer_setDisplayDensityDpi(JNIEnv*, jclass, static void android_view_ThreadedRenderer_initDisplayInfo(JNIEnv*, jclass, jint physicalWidth, jint physicalHeight, jfloat refreshRate, - jfloat maxRefreshRate, jint wideColorDataspace, jlong appVsyncOffsetNanos, jlong presentationDeadlineNanos) { DeviceInfo::setWidth(physicalWidth); DeviceInfo::setHeight(physicalHeight); DeviceInfo::setRefreshRate(refreshRate); - DeviceInfo::setMaxRefreshRate(maxRefreshRate); DeviceInfo::setWideColorDataspace(static_cast<ADataSpace>(wideColorDataspace)); DeviceInfo::setAppVsyncOffsetNanos(appVsyncOffsetNanos); DeviceInfo::setPresentationDeadlineNanos(presentationDeadlineNanos); @@ -735,7 +733,7 @@ static const JNINativeMethod gMethods[] = { {"nSetForceDark", "(JZ)V", (void*)android_view_ThreadedRenderer_setForceDark}, {"nSetDisplayDensityDpi", "(I)V", (void*)android_view_ThreadedRenderer_setDisplayDensityDpi}, - {"nInitDisplayInfo", "(IIFFIJJ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo}, + {"nInitDisplayInfo", "(IIFIJJ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo}, {"preload", "()V", (void*)android_view_ThreadedRenderer_preload}, }; diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp index 8b35d96aeac8..80239687a7fb 100644 --- a/libs/hwui/jni/android_graphics_RenderNode.cpp +++ b/libs/hwui/jni/android_graphics_RenderNode.cpp @@ -166,6 +166,31 @@ static jboolean android_view_RenderNode_setOutlineNone(CRITICAL_JNI_PARAMS_COMMA return true; } +static jboolean android_view_RenderNode_clearStretch(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr) { + RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr); + auto& stretch = renderNode->mutateStagingProperties() + .mutateLayerProperties().mutableStretchEffect(); + if (stretch.isEmpty()) { + return false; + } + stretch.setEmpty(); + renderNode->setPropertyFieldsDirty(RenderNode::GENERIC); + return true; +} + +static jboolean android_view_RenderNode_stretch(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr, + jfloat left, jfloat top, jfloat right, jfloat bottom, jfloat vX, jfloat vY, jfloat max) { + RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr); + renderNode->mutateStagingProperties().mutateLayerProperties().mutableStretchEffect().mergeWith( + StretchEffect{ + .stretchArea = SkRect::MakeLTRB(left, top, right, bottom), + .stretchDirection = {.fX = vX, .fY = vY}, + .maxStretchAmount = max + }); + renderNode->setPropertyFieldsDirty(RenderNode::GENERIC); + return true; +} + static jboolean android_view_RenderNode_hasShadow(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr) { RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr); return renderNode->stagingProperties().hasShadow(); @@ -678,6 +703,8 @@ static const JNINativeMethod gMethods[] = { { "nSetOutlinePath", "(JJF)Z", (void*) android_view_RenderNode_setOutlinePath }, { "nSetOutlineEmpty", "(J)Z", (void*) android_view_RenderNode_setOutlineEmpty }, { "nSetOutlineNone", "(J)Z", (void*) android_view_RenderNode_setOutlineNone }, + { "nClearStretch", "(J)Z", (void*) android_view_RenderNode_clearStretch }, + { "nStretch", "(JFFFFFFF)Z", (void*) android_view_RenderNode_stretch }, { "nHasShadow", "(J)Z", (void*) android_view_RenderNode_hasShadow }, { "nSetSpotShadowColor", "(JI)Z", (void*) android_view_RenderNode_setSpotShadowColor }, { "nGetSpotShadowColor", "(J)I", (void*) android_view_RenderNode_getSpotShadowColor }, diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp index b944310d8822..5a972f56ea87 100644 --- a/libs/hwui/jni/fonts/Font.cpp +++ b/libs/hwui/jni/fonts/Font.cpp @@ -34,6 +34,7 @@ #include <hwui/Typeface.h> #include <minikin/FontFamily.h> #include <minikin/FontFileParser.h> +#include <minikin/LocaleList.h> #include <ui/FatVector.h> #include <memory> @@ -79,7 +80,8 @@ static void Font_Builder_addAxis(CRITICAL_JNI_PARAMS_COMMA jlong builderPtr, jin // Regular JNI static jlong Font_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr, jobject buffer, - jstring filePath, jint weight, jboolean italic, jint ttcIndex) { + jstring filePath, jstring langTags, jint weight, jboolean italic, + jint ttcIndex) { NPE_CHECK_RETURN_ZERO(env, buffer); std::unique_ptr<NativeFontBuilder> builder(toBuilder(builderPtr)); const void* fontPtr = env->GetDirectBufferAddress(buffer); @@ -94,6 +96,7 @@ static jlong Font_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr, jo return 0; } ScopedUtfChars fontPath(env, filePath); + ScopedUtfChars langTagStr(env, langTags); jobject fontRef = MakeGlobalRefOrDie(env, buffer); sk_sp<SkData> data(SkData::MakeWithProc(fontPtr, fontSize, release_global_ref, reinterpret_cast<void*>(fontRef))); @@ -105,8 +108,13 @@ static jlong Font_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr, jo "Failed to create internal object. maybe invalid font data."); return 0; } - std::shared_ptr<minikin::Font> font = minikin::Font::Builder(minikinFont).setWeight(weight) - .setSlant(static_cast<minikin::FontStyle::Slant>(italic)).build(); + uint32_t localeListId = minikin::registerLocaleList(langTagStr.c_str()); + std::shared_ptr<minikin::Font> font = + minikin::Font::Builder(minikinFont) + .setWeight(weight) + .setSlant(static_cast<minikin::FontStyle::Slant>(italic)) + .setLocaleListId(localeListId) + .build(); return reinterpret_cast<jlong>(new FontWrapper(std::move(font))); } @@ -129,12 +137,9 @@ static jlong Font_Builder_clone(JNIEnv* env, jobject clazz, jlong fontPtr, jlong sk_sp<SkTypeface> newTypeface = minikinSkia->GetSkTypeface()->makeClone(args); std::shared_ptr<minikin::MinikinFont> newMinikinFont = std::make_shared<MinikinFontSkia>( - std::move(newTypeface), - minikinSkia->GetFontData(), - minikinSkia->GetFontSize(), - minikinSkia->getFilePath(), - minikinSkia->GetFontIndex(), - builder->axes); + std::move(newTypeface), minikinSkia->GetSourceId(), minikinSkia->GetFontData(), + minikinSkia->GetFontSize(), minikinSkia->getFilePath(), minikinSkia->GetFontIndex(), + builder->axes); std::shared_ptr<minikin::Font> newFont = minikin::Font::Builder(newMinikinFont) .setWeight(weight) .setSlant(static_cast<minikin::FontStyle::Slant>(italic)) @@ -142,12 +147,8 @@ static jlong Font_Builder_clone(JNIEnv* env, jobject clazz, jlong fontPtr, jlong return reinterpret_cast<jlong>(new FontWrapper(std::move(newFont))); } -// Critical Native -static jlong Font_Builder_getReleaseNativeFont(CRITICAL_JNI_PARAMS) { - return reinterpret_cast<jlong>(releaseFont); -} - /////////////////////////////////////////////////////////////////////////////// +// Font JNI functions // Fast Native static jfloat Font_getGlyphBounds(JNIEnv* env, jobject, jlong fontHandle, jint glyphId, @@ -188,51 +189,98 @@ static jfloat Font_getFontMetrics(JNIEnv* env, jobject, jlong fontHandle, jlong } // Critical Native -static jlong Font_getNativeFontPtr(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) { - FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle); - return reinterpret_cast<jlong>(font->font.get()); +static jlong Font_getMinikinFontPtr(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); + return reinterpret_cast<jlong>(font->font->typeface().get()); } // Critical Native -static jlong Font_GetBufferAddress(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) { - FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle); - const void* bufferPtr = font->font->typeface()->GetFontData(); - return reinterpret_cast<jlong>(bufferPtr); +static jlong Font_cloneFont(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); + std::shared_ptr<minikin::Font> ref = font->font; + return reinterpret_cast<jlong>(new FontWrapper(std::move(ref))); } -/////////////////////////////////////////////////////////////////////////////// - -struct FontBufferWrapper { - FontBufferWrapper(const std::shared_ptr<minikin::MinikinFont>& font) : minikinFont(font) {} - // MinikinFont holds a shared pointer of SkTypeface which has reference to font data. - std::shared_ptr<minikin::MinikinFont> minikinFont; -}; +// Fast Native +static jobject Font_newByteBuffer(JNIEnv* env, jobject, jlong fontPtr) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); + const std::shared_ptr<minikin::MinikinFont>& minikinFont = font->font->typeface(); + return env->NewDirectByteBuffer(const_cast<void*>(minikinFont->GetFontData()), + minikinFont->GetFontSize()); +} -static void unrefBuffer(jlong nativePtr) { - FontBufferWrapper* wrapper = reinterpret_cast<FontBufferWrapper*>(nativePtr); - delete wrapper; +// Critical Native +static jlong Font_getBufferAddress(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); + return reinterpret_cast<jlong>(font->font->typeface()->GetFontData()); } // Critical Native -static jlong FontBufferHelper_refFontBuffer(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) { - const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle); - return reinterpret_cast<jlong>(new FontBufferWrapper(font->typeface())); +static jlong Font_getReleaseNativeFontFunc(CRITICAL_JNI_PARAMS) { + return reinterpret_cast<jlong>(releaseFont); } // Fast Native -static jobject FontBufferHelper_wrapByteBuffer(JNIEnv* env, jobject, jlong nativePtr) { - FontBufferWrapper* wrapper = reinterpret_cast<FontBufferWrapper*>(nativePtr); - return env->NewDirectByteBuffer( - const_cast<void*>(wrapper->minikinFont->GetFontData()), - wrapper->minikinFont->GetFontSize()); +static jstring Font_getFontPath(JNIEnv* env, jobject, jlong fontPtr) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); + const std::shared_ptr<minikin::MinikinFont>& minikinFont = font->font->typeface(); + const std::string& path = minikinFont->GetFontPath(); + if (path.empty()) { + return nullptr; + } + return env->NewStringUTF(path.c_str()); +} + +// Fast Native +static jstring Font_getLocaleList(JNIEnv* env, jobject, jlong fontPtr) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); + uint32_t localeListId = font->font->getLocaleListId(); + if (localeListId == 0) { + return nullptr; + } + std::string langTags = minikin::getLocaleString(localeListId); + if (langTags.empty()) { + return nullptr; + } + return env->NewStringUTF(langTags.c_str()); } // Critical Native -static jlong FontBufferHelper_getReleaseFunc(CRITICAL_JNI_PARAMS) { - return reinterpret_cast<jlong>(unrefBuffer); +static jint Font_getPackedStyle(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); + uint32_t weight = font->font->style().weight(); + uint32_t isItalic = font->font->style().slant() == minikin::FontStyle::Slant::ITALIC ? 1 : 0; + return (isItalic << 16) | weight; } -/////////////////////////////////////////////////////////////////////////////// +// Critical Native +static jint Font_getIndex(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); + const std::shared_ptr<minikin::MinikinFont>& minikinFont = font->font->typeface(); + return minikinFont->GetFontIndex(); +} + +// Critical Native +static jint Font_getAxisCount(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); + const std::shared_ptr<minikin::MinikinFont>& minikinFont = font->font->typeface(); + return minikinFont->GetAxes().size(); +} + +// Critical Native +static jlong Font_getAxisInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr, jint index) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); + const std::shared_ptr<minikin::MinikinFont>& minikinFont = font->font->typeface(); + minikin::FontVariation var = minikinFont->GetAxes().at(index); + uint32_t floatBinary = *reinterpret_cast<const uint32_t*>(&var.value); + return (static_cast<uint64_t>(var.axisTag) << 32) | static_cast<uint64_t>(floatBinary); +} + +// Critical Native +static jint Font_getSourceId(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); + return font->font->typeface()->GetSourceId(); +} // Fast Native static jlong FontFileUtil_getFontRevision(JNIEnv* env, jobject, jobject buffer, jint index) { @@ -302,24 +350,29 @@ static jint FontFileUtil_isPostScriptType1Font(JNIEnv* env, jobject, jobject buf /////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gFontBuilderMethods[] = { - { "nInitBuilder", "()J", (void*) Font_Builder_initBuilder }, - { "nAddAxis", "(JIF)V", (void*) Font_Builder_addAxis }, - { "nBuild", "(JLjava/nio/ByteBuffer;Ljava/lang/String;IZI)J", (void*) Font_Builder_build }, - { "nClone", "(JJIZI)J", (void*) Font_Builder_clone }, - { "nGetReleaseNativeFont", "()J", (void*) Font_Builder_getReleaseNativeFont }, + {"nInitBuilder", "()J", (void*)Font_Builder_initBuilder}, + {"nAddAxis", "(JIF)V", (void*)Font_Builder_addAxis}, + {"nBuild", "(JLjava/nio/ByteBuffer;Ljava/lang/String;Ljava/lang/String;IZI)J", + (void*)Font_Builder_build}, + {"nClone", "(JJIZI)J", (void*)Font_Builder_clone}, }; static const JNINativeMethod gFontMethods[] = { - { "nGetGlyphBounds", "(JIJLandroid/graphics/RectF;)F", (void*) Font_getGlyphBounds }, - { "nGetFontMetrics", "(JJLandroid/graphics/Paint$FontMetrics;)F", (void*) Font_getFontMetrics }, - { "nGetNativeFontPtr", "(J)J", (void*) Font_getNativeFontPtr }, - { "nGetFontBufferAddress", "(J)J", (void*) Font_GetBufferAddress }, -}; - -static const JNINativeMethod gFontBufferHelperMethods[] = { - { "nRefFontBuffer", "(J)J", (void*) FontBufferHelper_refFontBuffer }, - { "nWrapByteBuffer", "(J)Ljava/nio/ByteBuffer;", (void*) FontBufferHelper_wrapByteBuffer }, - { "nGetReleaseFunc", "()J", (void*) FontBufferHelper_getReleaseFunc }, + {"nGetMinikinFontPtr", "(J)J", (void*)Font_getMinikinFontPtr}, + {"nCloneFont", "(J)J", (void*)Font_cloneFont}, + {"nNewByteBuffer", "(J)Ljava/nio/ByteBuffer;", (void*)Font_newByteBuffer}, + {"nGetBufferAddress", "(J)J", (void*)Font_getBufferAddress}, + {"nGetReleaseNativeFont", "()J", (void*)Font_getReleaseNativeFontFunc}, + {"nGetGlyphBounds", "(JIJLandroid/graphics/RectF;)F", (void*)Font_getGlyphBounds}, + {"nGetFontMetrics", "(JJLandroid/graphics/Paint$FontMetrics;)F", + (void*)Font_getFontMetrics}, + {"nGetFontPath", "(J)Ljava/lang/String;", (void*)Font_getFontPath}, + {"nGetLocaleList", "(J)Ljava/lang/String;", (void*)Font_getLocaleList}, + {"nGetPackedStyle", "(J)I", (void*)Font_getPackedStyle}, + {"nGetIndex", "(J)I", (void*)Font_getIndex}, + {"nGetAxisCount", "(J)I", (void*)Font_getAxisCount}, + {"nGetAxisInfo", "(JI)J", (void*)Font_getAxisInfo}, + {"nGetSourceId", "(J)I", (void*)Font_getSourceId}, }; static const JNINativeMethod gFontFileUtilMethods[] = { @@ -335,8 +388,6 @@ int register_android_graphics_fonts_Font(JNIEnv* env) { NELEM(gFontBuilderMethods)) + RegisterMethodsOrDie(env, "android/graphics/fonts/Font", gFontMethods, NELEM(gFontMethods)) + - RegisterMethodsOrDie(env, "android/graphics/fonts/NativeFontBufferHelper", - gFontBufferHelperMethods, NELEM(gFontBufferHelperMethods)) + RegisterMethodsOrDie(env, "android/graphics/fonts/FontFileUtil", gFontFileUtilMethods, NELEM(gFontFileUtilMethods)); } @@ -362,10 +413,15 @@ std::shared_ptr<minikin::MinikinFont> createMinikinFontSkia( if (face == nullptr) { return nullptr; } - return std::make_shared<MinikinFontSkia>(std::move(face), fontPtr, fontSize, + return std::make_shared<MinikinFontSkia>(std::move(face), getNewSourceId(), fontPtr, fontSize, fontPath, ttcIndex, axes); } +int getNewSourceId() { + static std::atomic<int> sSourceId = {0}; + return sSourceId++; +} + } // namespace fonts } // namespace android diff --git a/libs/hwui/jni/fonts/Font.h b/libs/hwui/jni/fonts/Font.h index b5d20bf8cc3c..4bf60ee85657 100644 --- a/libs/hwui/jni/fonts/Font.h +++ b/libs/hwui/jni/fonts/Font.h @@ -33,6 +33,8 @@ std::shared_ptr<minikin::MinikinFont> createMinikinFontSkia( sk_sp<SkData>&& data, std::string_view fontPath, const void *fontPtr, size_t fontSize, int ttcIndex, const std::vector<minikin::FontVariation>& axes); +int getNewSourceId(); + } // namespace fonts } // namespace android diff --git a/libs/hwui/jni/fonts/FontFamily.cpp b/libs/hwui/jni/fonts/FontFamily.cpp index 37e52766f2ef..b68213549938 100644 --- a/libs/hwui/jni/fonts/FontFamily.cpp +++ b/libs/hwui/jni/fonts/FontFamily.cpp @@ -83,19 +83,57 @@ static jlong FontFamily_Builder_GetReleaseFunc(CRITICAL_JNI_PARAMS) { return reinterpret_cast<jlong>(releaseFontFamily); } +// FastNative +static jstring FontFamily_getLangTags(JNIEnv* env, jobject, jlong familyPtr) { + FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(familyPtr); + uint32_t localeListId = family->family->localeListId(); + if (localeListId == 0) { + return nullptr; + } + std::string langTags = minikin::getLocaleString(localeListId); + return env->NewStringUTF(langTags.c_str()); +} + +// CriticalNative +static jint FontFamily_getVariant(CRITICAL_JNI_PARAMS_COMMA jlong familyPtr) { + FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(familyPtr); + return static_cast<jint>(family->family->variant()); +} + +// CriticalNative +static jint FontFamily_getFontSize(CRITICAL_JNI_PARAMS_COMMA jlong familyPtr) { + FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(familyPtr); + return family->family->getNumFonts(); +} + +// CriticalNative +static jlong FontFamily_getFont(CRITICAL_JNI_PARAMS_COMMA jlong familyPtr, jint index) { + FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(familyPtr); + std::shared_ptr<minikin::Font> font = family->family->getFontRef(index); + return reinterpret_cast<jlong>(new FontWrapper(std::move(font))); +} + /////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gFontFamilyBuilderMethods[] = { { "nInitBuilder", "()J", (void*) FontFamily_Builder_initBuilder }, { "nAddFont", "(JJ)V", (void*) FontFamily_Builder_addFont }, { "nBuild", "(JLjava/lang/String;IZ)J", (void*) FontFamily_Builder_build }, - { "nGetReleaseNativeFamily", "()J", (void*) FontFamily_Builder_GetReleaseFunc }, }; +static const JNINativeMethod gFontFamilyMethods[] = { + {"nGetFontSize", "(J)I", (void*)FontFamily_getFontSize}, + {"nGetFont", "(JI)J", (void*)FontFamily_getFont}, + {"nGetLangTags", "(J)Ljava/lang/String;", (void*)FontFamily_getLangTags}, + {"nGetVariant", "(J)I", (void*)FontFamily_getVariant}, +}; + int register_android_graphics_fonts_FontFamily(JNIEnv* env) { return RegisterMethodsOrDie(env, "android/graphics/fonts/FontFamily$Builder", - gFontFamilyBuilderMethods, NELEM(gFontFamilyBuilderMethods)); + gFontFamilyBuilderMethods, NELEM(gFontFamilyBuilderMethods)) + + RegisterMethodsOrDie(env, "android/graphics/fonts/FontFamily", gFontFamilyMethods, + NELEM(gFontFamilyMethods)); } } diff --git a/libs/hwui/jni/fonts/NativeFont.cpp b/libs/hwui/jni/fonts/NativeFont.cpp deleted file mode 100644 index c5c5d464ccac..000000000000 --- a/libs/hwui/jni/fonts/NativeFont.cpp +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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. - */ - -#undef LOG_TAG -#define LOG_TAG "Minikin" - -#include "Font.h" -#include "SkData.h" -#include "SkFont.h" -#include "SkFontMetrics.h" -#include "SkFontMgr.h" -#include "SkRefCnt.h" -#include "SkTypeface.h" -#include "GraphicsJNI.h" -#include <nativehelper/ScopedUtfChars.h> -#include "Utils.h" -#include "FontUtils.h" - -#include <hwui/MinikinSkia.h> -#include <hwui/Paint.h> -#include <hwui/Typeface.h> -#include <minikin/FontFamily.h> -#include <minikin/LocaleList.h> -#include <ui/FatVector.h> - -#include <memory> - -namespace android { - -// Critical Native -static jint NativeFont_getFamilyCount(CRITICAL_JNI_PARAMS_COMMA jlong typefaceHandle) { - Typeface* tf = reinterpret_cast<Typeface*>(typefaceHandle); - return tf->fFontCollection->getFamilies().size(); -} - -// Critical Native -static jlong NativeFont_getFamily(CRITICAL_JNI_PARAMS_COMMA jlong typefaceHandle, jint index) { - Typeface* tf = reinterpret_cast<Typeface*>(typefaceHandle); - return reinterpret_cast<jlong>(tf->fFontCollection->getFamilies()[index].get()); - -} - -// Fast Native -static jstring NativeFont_getLocaleList(JNIEnv* env, jobject, jlong familyHandle) { - minikin::FontFamily* family = reinterpret_cast<minikin::FontFamily*>(familyHandle); - uint32_t localeListId = family->localeListId(); - return env->NewStringUTF(minikin::getLocaleString(localeListId).c_str()); -} - -// Critical Native -static jint NativeFont_getFontCount(CRITICAL_JNI_PARAMS_COMMA jlong familyHandle) { - minikin::FontFamily* family = reinterpret_cast<minikin::FontFamily*>(familyHandle); - return family->getNumFonts(); -} - -// Critical Native -static jlong NativeFont_getFont(CRITICAL_JNI_PARAMS_COMMA jlong familyHandle, jint index) { - minikin::FontFamily* family = reinterpret_cast<minikin::FontFamily*>(familyHandle); - return reinterpret_cast<jlong>(family->getFont(index)); -} - -// Critical Native -static jlong NativeFont_getFontInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) { - const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle); - MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->typeface().get()); - - uint64_t result = font->style().weight(); - result |= font->style().slant() == minikin::FontStyle::Slant::ITALIC ? 0x10000 : 0x00000; - result |= ((static_cast<uint64_t>(minikinSkia->GetFontIndex())) << 32); - result |= ((static_cast<uint64_t>(minikinSkia->GetAxes().size())) << 48); - return result; -} - -// Critical Native -static jlong NativeFont_getAxisInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle, jint index) { - const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle); - MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->typeface().get()); - const minikin::FontVariation& var = minikinSkia->GetAxes().at(index); - uint32_t floatBinary = *reinterpret_cast<const uint32_t*>(&var.value); - return (static_cast<uint64_t>(var.axisTag) << 32) | static_cast<uint64_t>(floatBinary); -} - -// FastNative -static jstring NativeFont_getFontPath(JNIEnv* env, jobject, jlong fontHandle) { - const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle); - MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->typeface().get()); - const std::string& filePath = minikinSkia->getFilePath(); - if (filePath.empty()) { - return nullptr; - } - return env->NewStringUTF(filePath.c_str()); -} - -/////////////////////////////////////////////////////////////////////////////// - -static const JNINativeMethod gNativeFontMethods[] = { - { "nGetFamilyCount", "(J)I", (void*) NativeFont_getFamilyCount }, - { "nGetFamily", "(JI)J", (void*) NativeFont_getFamily }, - { "nGetLocaleList", "(J)Ljava/lang/String;", (void*) NativeFont_getLocaleList }, - { "nGetFontCount", "(J)I", (void*) NativeFont_getFontCount }, - { "nGetFont", "(JI)J", (void*) NativeFont_getFont }, - { "nGetFontInfo", "(J)J", (void*) NativeFont_getFontInfo }, - { "nGetAxisInfo", "(JI)J", (void*) NativeFont_getAxisInfo }, - { "nGetFontPath", "(J)Ljava/lang/String;", (void*) NativeFont_getFontPath }, -}; - -int register_android_graphics_fonts_NativeFont(JNIEnv* env) { - return RegisterMethodsOrDie(env, "android/graphics/fonts/NativeFont", gNativeFontMethods, - NELEM(gNativeFontMethods)); -} - -} // namespace android diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp index 9785aa537f65..a6fb95832c03 100644 --- a/libs/hwui/jni/text/TextShaper.cpp +++ b/libs/hwui/jni/text/TextShaper.cpp @@ -23,13 +23,14 @@ #include <set> #include <algorithm> -#include "SkPaint.h" -#include "SkTypeface.h" #include <hwui/MinikinSkia.h> #include <hwui/MinikinUtils.h> #include <hwui/Paint.h> -#include <minikin/MinikinPaint.h> #include <minikin/MinikinFont.h> +#include <minikin/MinikinPaint.h> +#include "FontUtils.h" +#include "SkPaint.h" +#include "SkTypeface.h" namespace android { @@ -149,7 +150,8 @@ static jfloat TextShaper_Result_getY(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i // CriticalNative static jlong TextShaper_Result_getFont(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) { const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); - return reinterpret_cast<jlong>(layout->layout.getFont(i)); + std::shared_ptr<minikin::Font> fontRef = layout->layout.getFontRef(i); + return reinterpret_cast<jlong>(new FontWrapper(std::move(fontRef))); } // CriticalNative diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp index c6c9e9dc869a..71f533c3fc4f 100644 --- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp @@ -194,7 +194,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { canvas->concat(invertedMatrix); const SkIRect deviceBounds = canvas->getDeviceClipBounds(); - tmpSurface->draw(canvas, deviceBounds.fLeft, deviceBounds.fTop, nullptr); + tmpSurface->draw(canvas, deviceBounds.fLeft, deviceBounds.fTop); } } diff --git a/libs/hwui/pipeline/skia/LayerDrawable.cpp b/libs/hwui/pipeline/skia/LayerDrawable.cpp index 34df5ddbb210..471a7f7af3b1 100644 --- a/libs/hwui/pipeline/skia/LayerDrawable.cpp +++ b/libs/hwui/pipeline/skia/LayerDrawable.cpp @@ -34,7 +34,7 @@ void LayerDrawable::onDraw(SkCanvas* canvas) { } static inline SkScalar isIntegerAligned(SkScalar x) { - return fabsf(roundf(x) - x) <= NON_ZERO_EPSILON; + return MathUtils::isZero(roundf(x) - x); } // Disable filtering when there is no scaling in screen coordinates and the corners have the same diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp index 75815bb6e63d..c01021221f37 100644 --- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp +++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp @@ -20,6 +20,8 @@ #include "SkiaDisplayList.h" #include "utils/TraceUtils.h" +#include <include/effects/SkImageFilters.h> + #include <optional> namespace android { @@ -171,11 +173,25 @@ static bool layerNeedsPaint(const LayerProperties& properties, float alphaMultip SkPaint* paint) { if (alphaMultiplier < 1.0f || properties.alpha() < 255 || properties.xferMode() != SkBlendMode::kSrcOver || properties.getColorFilter() != nullptr || - properties.getImageFilter() != nullptr) { + properties.getImageFilter() != nullptr || !properties.getStretchEffect().isEmpty()) { paint->setAlpha(properties.alpha() * alphaMultiplier); paint->setBlendMode(properties.xferMode()); paint->setColorFilter(sk_ref_sp(properties.getColorFilter())); - paint->setImageFilter(sk_ref_sp(properties.getImageFilter())); + + sk_sp<SkImageFilter> imageFilter = sk_ref_sp(properties.getImageFilter()); + sk_sp<SkImageFilter> stretchFilter = properties.getStretchEffect().getImageFilter(); + sk_sp<SkImageFilter> filter; + if (imageFilter && stretchFilter) { + filter = SkImageFilters::Compose( + std::move(stretchFilter), + std::move(imageFilter) + ); + } else if (stretchFilter) { + filter = std::move(stretchFilter); + } else { + filter = std::move(imageFilter); + } + paint->setImageFilter(std::move(filter)); return true; } return false; diff --git a/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp b/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp index 3b8caeb3aab1..7cfccb56382c 100644 --- a/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp +++ b/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp @@ -52,7 +52,7 @@ void StartReorderBarrierDrawable::onDraw(SkCanvas* canvas) { RenderNodeDrawable* childNode = mChildren[drawIndex]; SkASSERT(childNode); const float casterZ = childNode->getNodeProperties().getZ(); - if (casterZ >= -NON_ZERO_EPSILON) { // draw only children with negative Z + if (casterZ >= -MathUtils::NON_ZERO_EPSILON) { // draw only children with negative Z return; } SkAutoCanvasRestore acr(canvas, true); @@ -86,7 +86,7 @@ void EndReorderBarrierDrawable::onDraw(SkCanvas* canvas) { const size_t endIndex = zChildren.size(); while (drawIndex < endIndex // draw only children with positive Z - && zChildren[drawIndex]->getNodeProperties().getZ() <= NON_ZERO_EPSILON) + && zChildren[drawIndex]->getNodeProperties().getZ() <= MathUtils::NON_ZERO_EPSILON) drawIndex++; size_t shadowIndex = drawIndex; diff --git a/libs/hwui/renderthread/CacheManager.cpp b/libs/hwui/renderthread/CacheManager.cpp index 85924c5e8939..d998e5031984 100644 --- a/libs/hwui/renderthread/CacheManager.cpp +++ b/libs/hwui/renderthread/CacheManager.cpp @@ -101,7 +101,8 @@ void CacheManager::trimMemory(TrimMemoryMode mode) { return; } - mGrContext->flushAndSubmit(); + // flush and submit all work to the gpu and wait for it to finish + mGrContext->flushAndSubmit(/*syncCpu=*/true); switch (mode) { case TrimMemoryMode::Complete: @@ -119,11 +120,6 @@ void CacheManager::trimMemory(TrimMemoryMode mode) { SkGraphics::SetFontCacheLimit(mMaxCpuFontCacheBytes); break; } - - // We must sync the cpu to make sure deletions of resources still queued up on the GPU actually - // happen. - mGrContext->flush({}); - mGrContext->submit(true); } void CacheManager::trimStaleResources() { diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index eacabfd1dbf9..65afcc3a2558 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -108,7 +108,6 @@ CanvasContext::CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode->makeRoot(); mRenderNodes.emplace_back(rootRenderNode); mProfiler.setDensity(DeviceInfo::getDensity()); - setRenderAheadDepth(Properties::defaultRenderAhead); } CanvasContext::~CanvasContext() { @@ -157,22 +156,17 @@ static void setBufferCount(ANativeWindow* window) { void CanvasContext::setSurface(ANativeWindow* window, bool enableTimeout) { ATRACE_CALL(); - if (mRenderAheadDepth == 0 && DeviceInfo::get()->getMaxRefreshRate() > 66.6f) { - mFixedRenderAhead = false; - mRenderAheadCapacity = 1; - } else { - mFixedRenderAhead = true; - mRenderAheadCapacity = mRenderAheadDepth; - } - if (window) { + int extraBuffers = 0; + native_window_get_extra_buffer_count(window, &extraBuffers); + mNativeSurface = std::make_unique<ReliableSurface>(window); mNativeSurface->init(); if (enableTimeout) { // TODO: Fix error handling & re-shorten timeout ANativeWindow_setDequeueTimeout(window, 4000_ms); } - mNativeSurface->setExtraBufferCount(mRenderAheadCapacity); + mNativeSurface->setExtraBufferCount(extraBuffers); } else { mNativeSurface = nullptr; } @@ -439,24 +433,6 @@ void CanvasContext::notifyFramePending() { mRenderThread.pushBackFrameCallback(this); } -void CanvasContext::setPresentTime() { - int64_t presentTime = NATIVE_WINDOW_TIMESTAMP_AUTO; - int renderAhead = 0; - const auto frameIntervalNanos = mRenderThread.timeLord().frameIntervalNanos(); - if (mFixedRenderAhead) { - renderAhead = std::min(mRenderAheadDepth, mRenderAheadCapacity); - } else if (frameIntervalNanos < 15_ms) { - renderAhead = std::min(1, static_cast<int>(mRenderAheadCapacity)); - } - - if (renderAhead) { - presentTime = mCurrentFrameInfo->get(FrameInfoIndex::Vsync) + - (frameIntervalNanos * (renderAhead + 1)) - DeviceInfo::get()->getAppOffset() + - (frameIntervalNanos / 2); - } - native_window_set_buffers_timestamp(mNativeSurface->getNativeWindow(), presentTime); -} - void CanvasContext::draw() { SkRect dirty; mDamageAccumulator.finish(&dirty); @@ -476,8 +452,6 @@ void CanvasContext::draw() { mCurrentFrameInfo->markIssueDrawCommandsStart(); Frame frame = mRenderPipeline->getFrame(); - setPresentTime(); - SkRect windowDirty = computeDirtyRect(frame, &dirty); bool drew = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry, &mLayerUpdateQueue, @@ -490,9 +464,11 @@ void CanvasContext::draw() { if (mNativeSurface) { // TODO(b/165985262): measure performance impact - if (const auto vsyncId = mCurrentFrameInfo->get(FrameInfoIndex::FrameTimelineVsyncId); - vsyncId != UiFrameInfoBuilder::INVALID_VSYNC_ID) { - native_window_set_frame_timeline_vsync(mNativeSurface->getNativeWindow(), vsyncId); + const auto vsyncId = mCurrentFrameInfo->get(FrameInfoIndex::FrameTimelineVsyncId); + if (vsyncId != UiFrameInfoBuilder::INVALID_VSYNC_ID) { + const auto inputEventId = mCurrentFrameInfo->get(FrameInfoIndex::NewestInputEvent); + native_window_set_frame_timeline_info(mNativeSurface->getNativeWindow(), vsyncId, + inputEventId); } } @@ -761,14 +737,6 @@ bool CanvasContext::surfaceRequiresRedraw() { return width != mLastFrameWidth || height != mLastFrameHeight; } -void CanvasContext::setRenderAheadDepth(int renderAhead) { - if (renderAhead > 2 || renderAhead < 0 || mNativeSurface) { - return; - } - mFixedRenderAhead = true; - mRenderAheadDepth = static_cast<uint32_t>(renderAhead); -} - SkRect CanvasContext::computeDirtyRect(const Frame& frame, SkRect* dirty) { if (frame.width() != mLastFrameWidth || frame.height() != mLastFrameHeight) { // can't rely on prior content of window if viewport size changes diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h index cc4eb3285bbe..b31883b9ae94 100644 --- a/libs/hwui/renderthread/CanvasContext.h +++ b/libs/hwui/renderthread/CanvasContext.h @@ -193,9 +193,6 @@ public: return mUseForceDark; } - // Must be called before setSurface - void setRenderAheadDepth(int renderAhead); - SkISize getNextFrameSize() const; private: @@ -211,7 +208,6 @@ private: bool isSwapChainStuffed(); bool surfaceRequiresRedraw(); - void setPresentTime(); void setupPipelineSurface(); SkRect computeDirtyRect(const Frame& frame, SkRect* dirty); @@ -232,9 +228,6 @@ private: // painted onto its surface. bool mIsDirty = false; SwapBehavior mSwapBehavior = SwapBehavior::kSwap_default; - bool mFixedRenderAhead = false; - uint32_t mRenderAheadDepth = 0; - uint32_t mRenderAheadCapacity = 0; struct SwapHistory { SkRect damage; nsecs_t vsyncTime; diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index b51f6dcfc66f..0ade8dde12eb 100644 --- a/libs/hwui/renderthread/RenderProxy.cpp +++ b/libs/hwui/renderthread/RenderProxy.cpp @@ -295,11 +295,6 @@ void RenderProxy::setForceDark(bool enable) { mRenderThread.queue().post([this, enable]() { mContext->setForceDark(enable); }); } -void RenderProxy::setRenderAheadDepth(int renderAhead) { - mRenderThread.queue().post( - [context = mContext, renderAhead] { context->setRenderAheadDepth(renderAhead); }); -} - int RenderProxy::copySurfaceInto(ANativeWindow* window, int left, int top, int right, int bottom, SkBitmap* bitmap) { auto& thread = RenderThread::getInstance(); diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h index 33dabc9895b1..a4adb16a930e 100644 --- a/libs/hwui/renderthread/RenderProxy.h +++ b/libs/hwui/renderthread/RenderProxy.h @@ -123,23 +123,6 @@ public: void removeFrameMetricsObserver(FrameMetricsObserver* observer); void setForceDark(bool enable); - /** - * Sets a render-ahead depth on the backing renderer. This will increase latency by - * <swapInterval> * renderAhead and increase memory usage by (3 + renderAhead) * <resolution>. - * In return the renderer will be less susceptible to jitter, resulting in a smoother animation. - * - * Not recommended to use in response to anything touch driven, but for canned animations - * where latency is not a concern careful use may be beneficial. - * - * Note that when increasing this there will be a frame gap of N frames where N is - * renderAhead - <current renderAhead>. When decreasing this if there are any pending - * frames they will retain their prior renderAhead value, so it will take a few frames - * for the decrease to flush through. - * - * @param renderAhead How far to render ahead, must be in the range [0..2] - */ - void setRenderAheadDepth(int renderAhead); - static int copySurfaceInto(ANativeWindow* window, int left, int top, int right, int bottom, SkBitmap* bitmap); static void prepareToDraw(Bitmap& bitmap); diff --git a/libs/hwui/tests/common/TestContext.cpp b/libs/hwui/tests/common/TestContext.cpp index 06f158f25fc5..6a9a98d6743b 100644 --- a/libs/hwui/tests/common/TestContext.cpp +++ b/libs/hwui/tests/common/TestContext.cpp @@ -40,9 +40,9 @@ const DisplayInfo& getDisplayInfo() { return info; } -const DisplayConfig& getActiveDisplayConfig() { - static DisplayConfig config = [] { - DisplayConfig config; +const ui::DisplayMode& getActiveDisplayMode() { + static ui::DisplayMode config = [] { + ui::DisplayMode config; #if HWUI_NULL_GPU config.resolution = ui::Size(1080, 1920); config.xDpi = config.yDpi = 320.f; @@ -51,7 +51,7 @@ const DisplayConfig& getActiveDisplayConfig() { const sp<IBinder> token = SurfaceComposerClient::getInternalDisplayToken(); LOG_ALWAYS_FATAL_IF(!token, "%s: No internal display", __FUNCTION__); - const status_t status = SurfaceComposerClient::getActiveDisplayConfig(token, &config); + const status_t status = SurfaceComposerClient::getActiveDisplayMode(token, &config); LOG_ALWAYS_FATAL_IF(status, "%s: Failed to get active display config", __FUNCTION__); #endif return config; diff --git a/libs/hwui/tests/common/TestContext.h b/libs/hwui/tests/common/TestContext.h index a012ecb1a1d3..7d2f6d8ea731 100644 --- a/libs/hwui/tests/common/TestContext.h +++ b/libs/hwui/tests/common/TestContext.h @@ -23,8 +23,8 @@ #include <gui/Surface.h> #include <gui/SurfaceComposerClient.h> #include <gui/SurfaceControl.h> -#include <ui/DisplayConfig.h> #include <ui/DisplayInfo.h> +#include <ui/DisplayMode.h> #include <utils/Looper.h> #include <atomic> @@ -37,10 +37,10 @@ namespace uirenderer { namespace test { const DisplayInfo& getDisplayInfo(); -const DisplayConfig& getActiveDisplayConfig(); +const ui::DisplayMode& getActiveDisplayMode(); inline const ui::Size& getActiveDisplayResolution() { - return getActiveDisplayConfig().resolution; + return getActiveDisplayMode().resolution; } class TestContext { diff --git a/libs/hwui/tests/common/TestScene.h b/libs/hwui/tests/common/TestScene.h index 74a039b3d090..91022cfe734b 100644 --- a/libs/hwui/tests/common/TestScene.h +++ b/libs/hwui/tests/common/TestScene.h @@ -38,7 +38,6 @@ public: int count = 0; int reportFrametimeWeight = 0; bool renderOffscreen = true; - int renderAhead = 0; }; template <class T> diff --git a/libs/hwui/tests/macrobench/TestSceneRunner.cpp b/libs/hwui/tests/macrobench/TestSceneRunner.cpp index eda5d2266dcf..8c7d2612f39b 100644 --- a/libs/hwui/tests/macrobench/TestSceneRunner.cpp +++ b/libs/hwui/tests/macrobench/TestSceneRunner.cpp @@ -153,11 +153,6 @@ void run(const TestScene::Info& info, const TestScene::Options& opts, proxy->resetProfileInfo(); proxy->fence(); - if (opts.renderAhead) { - usleep(33000); - } - proxy->setRenderAheadDepth(opts.renderAhead); - ModifiedMovingAverage<double> avgMs(opts.reportFrametimeWeight); nsecs_t start = systemTime(SYSTEM_TIME_MONOTONIC); diff --git a/libs/hwui/tests/macrobench/main.cpp b/libs/hwui/tests/macrobench/main.cpp index 88d33c315a09..174a14080eff 100644 --- a/libs/hwui/tests/macrobench/main.cpp +++ b/libs/hwui/tests/macrobench/main.cpp @@ -69,7 +69,6 @@ OPTIONS: are offscreen rendered --benchmark_format Set output format. Possible values are tabular, json, csv --renderer=TYPE Sets the render pipeline to use. May be skiagl or skiavk - --render-ahead=NUM Sets how far to render-ahead. Must be 0 (default), 1, or 2. )"); } @@ -171,7 +170,6 @@ enum { Onscreen, Offscreen, Renderer, - RenderAhead, }; } @@ -187,7 +185,6 @@ static const struct option LONG_OPTIONS[] = { {"onscreen", no_argument, nullptr, LongOpts::Onscreen}, {"offscreen", no_argument, nullptr, LongOpts::Offscreen}, {"renderer", required_argument, nullptr, LongOpts::Renderer}, - {"render-ahead", required_argument, nullptr, LongOpts::RenderAhead}, {0, 0, 0, 0}}; static const char* SHORT_OPTIONS = "c:r:h"; @@ -286,16 +283,6 @@ void parseOptions(int argc, char* argv[]) { gOpts.renderOffscreen = true; break; - case LongOpts::RenderAhead: - if (!optarg) { - error = true; - } - gOpts.renderAhead = atoi(optarg); - if (gOpts.renderAhead < 0 || gOpts.renderAhead > 2) { - error = true; - } - break; - case 'h': printHelp(); exit(EXIT_SUCCESS); diff --git a/libs/hwui/tests/unit/TypefaceTests.cpp b/libs/hwui/tests/unit/TypefaceTests.cpp index 5d2aa2ff83c9..ab23448ab93f 100644 --- a/libs/hwui/tests/unit/TypefaceTests.cpp +++ b/libs/hwui/tests/unit/TypefaceTests.cpp @@ -57,7 +57,7 @@ std::shared_ptr<minikin::FontFamily> buildFamily(const char* fileName) { sk_sp<SkTypeface> typeface(fm->makeFromStream(std::move(fontData))); LOG_ALWAYS_FATAL_IF(typeface == nullptr, "Failed to make typeface from %s", fileName); std::shared_ptr<minikin::MinikinFont> font = - std::make_shared<MinikinFontSkia>(std::move(typeface), data, st.st_size, fileName, 0, + std::make_shared<MinikinFontSkia>(std::move(typeface), 0, data, st.st_size, fileName, 0, std::vector<minikin::FontVariation>()); std::vector<std::shared_ptr<minikin::Font>> fonts; fonts.push_back(minikin::Font::Builder(font).build()); diff --git a/libs/hwui/utils/MathUtils.h b/libs/hwui/utils/MathUtils.h index 62bf39ca8a7a..1d3f9d701eed 100644 --- a/libs/hwui/utils/MathUtils.h +++ b/libs/hwui/utils/MathUtils.h @@ -22,11 +22,11 @@ namespace android { namespace uirenderer { -#define NON_ZERO_EPSILON (0.001f) -#define ALPHA_EPSILON (0.001f) - class MathUtils { public: + static constexpr float NON_ZERO_EPSILON = 0.001f; + static constexpr float ALPHA_EPSILON = 0.001f; + /** * Check for floats that are close enough to zero. */ diff --git a/libs/incident/Android.bp b/libs/incident/Android.bp index d291ec001daf..438a92ef8fb5 100644 --- a/libs/incident/Android.bp +++ b/libs/incident/Android.bp @@ -95,7 +95,7 @@ cc_test { name: "libincident_test", test_config: "AndroidTest.xml", defaults: ["libincidentpriv_defaults"], - test_suites: ["device-tests", "mts"], + test_suites: ["device-tests", "mts-statsd"], compile_multilib: "both", multilib: { lib64: { diff --git a/libs/tracingproxy/Android.bp b/libs/tracingproxy/Android.bp new file mode 100644 index 000000000000..67f407ff7599 --- /dev/null +++ b/libs/tracingproxy/Android.bp @@ -0,0 +1,42 @@ +// Copyright (C) 2020 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. + +// Provides C++ wrappers for system services. + +cc_library_shared { + name: "libtracingproxy", + + aidl: { + export_aidl_headers: true, + include_dirs: [ + "frameworks/base/core/java", + ], + }, + + srcs: [ + ":ITracingServiceProxy.aidl", + ], + + shared_libs: [ + "libbinder", + "libutils", + ], + + cflags: [ + "-Wall", + "-Werror", + "-Wunused", + "-Wunreachable-code", + ], +} |