diff options
Diffstat (limited to 'libs')
69 files changed, 2528 insertions, 338 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java index 37f0067de453..089613853555 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java @@ -16,12 +16,11 @@ package androidx.window.common; -import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; +import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN; import static androidx.window.common.layout.CommonFoldingFeature.parseListFromString; -import android.annotation.NonNull; import android.content.Context; import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateManager; @@ -31,16 +30,23 @@ import android.text.TextUtils; import android.util.Log; import android.util.SparseIntArray; +import androidx.annotation.BinderThread; +import androidx.annotation.GuardedBy; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import androidx.window.common.layout.CommonFoldingFeature; import androidx.window.common.layout.DisplayFoldFeatureCommon; import com.android.internal.R; +import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -55,13 +61,6 @@ public final class DeviceStateManagerFoldingFeatureProducer private static final boolean DEBUG = false; /** - * Emulated device state - * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to - * {@link CommonFoldingFeature.State} map. - */ - private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray(); - - /** * Device state received via * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)}. * The identifier returned through {@link DeviceState#getIdentifier()} may not correspond 1:1 @@ -71,23 +70,40 @@ public final class DeviceStateManagerFoldingFeatureProducer * "rear display". Concurrent mode for example is activated via public API and can be active in * both the "open" and "half folded" device states. */ - private DeviceState mCurrentDeviceState = new DeviceState( - new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER, - "INVALID").build()); + // TODO: b/337820752 - Add @GuardedBy("mCurrentDeviceStateLock") after flag cleanup. + private DeviceState mCurrentDeviceState = INVALID_DEVICE_STATE; - private List<DeviceState> mSupportedStates; + /** + * Lock to synchronize access to {@link #mCurrentDeviceState}. + * + * <p>This lock is used to ensure thread-safety when accessing and modifying the + * {@link #mCurrentDeviceState} field. It is acquired by both the binder thread (if + * {@link Flags#wlinfoOncreate()} is enabled) and the main thread (if + * {@link Flags#wlinfoOncreate()} is disabled) to prevent race conditions and + * ensure data consistency. + */ + private final Object mCurrentDeviceStateLock = new Object(); @NonNull private final RawFoldingFeatureProducer mRawFoldSupplier; - private final boolean mIsHalfOpenedSupported; - - private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() { + @NonNull + private final DeviceStateMapper mDeviceStateMapper; + + @VisibleForTesting + final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() { + // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the getData() + // implementation. See https://errorprone.info/bugpattern/GuardedBy for limitations. + @SuppressWarnings("GuardedBy") + @BinderThread // When Flags.wlinfoOncreate() is enabled. + @MainThread // When Flags.wlinfoOncreate() is disabled. @Override public void onDeviceStateChanged(@NonNull DeviceState state) { - mCurrentDeviceState = state; - mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer - .this::notifyFoldingFeatureChange); + synchronized (mCurrentDeviceStateLock) { + mCurrentDeviceState = state; + mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer.this + ::notifyFoldingFeatureChangeLocked); + } } }; @@ -95,41 +111,14 @@ public final class DeviceStateManagerFoldingFeatureProducer @NonNull RawFoldingFeatureProducer rawFoldSupplier, @NonNull DeviceStateManager deviceStateManager) { mRawFoldSupplier = rawFoldSupplier; - String[] deviceStatePosturePairs = context.getResources() - .getStringArray(R.array.config_device_state_postures); - mSupportedStates = deviceStateManager.getSupportedDeviceStates(); - boolean isHalfOpenedSupported = false; - for (String deviceStatePosturePair : deviceStatePosturePairs) { - String[] deviceStatePostureMapping = deviceStatePosturePair.split(":"); - if (deviceStatePostureMapping.length != 2) { - if (DEBUG) { - Log.e(TAG, "Malformed device state posture pair: " - + deviceStatePosturePair); - } - continue; - } + mDeviceStateMapper = + new DeviceStateMapper(context, deviceStateManager.getSupportedDeviceStates()); - int deviceState; - int posture; - try { - deviceState = Integer.parseInt(deviceStatePostureMapping[0]); - posture = Integer.parseInt(deviceStatePostureMapping[1]); - } catch (NumberFormatException e) { - if (DEBUG) { - Log.e(TAG, "Failed to parse device state or posture: " - + deviceStatePosturePair, - e); - } - continue; - } - isHalfOpenedSupported = isHalfOpenedSupported - || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED; - mDeviceStateToPostureMap.put(deviceState, posture); - } - mIsHalfOpenedSupported = isHalfOpenedSupported; - if (mDeviceStateToPostureMap.size() > 0) { + if (!mDeviceStateMapper.isDeviceStateToPostureMapEmpty()) { + final Executor executor = + Flags.wlinfoOncreate() ? Runnable::run : context.getMainExecutor(); Objects.requireNonNull(deviceStateManager) - .registerCallback(context.getMainExecutor(), mDeviceStateCallback); + .registerCallback(executor, mDeviceStateCallback); } } @@ -137,50 +126,51 @@ public final class DeviceStateManagerFoldingFeatureProducer * Add a callback to mCallbacks if there is no device state. This callback will be run * once a device state is set. Otherwise,run the callback immediately. */ - private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback, - String displayFeaturesString) { - if (isCurrentStateValid()) { - callback.accept(calculateFoldingFeature(displayFeaturesString)); + private void runCallbackWhenValidState(@NonNull DeviceState state, + @NonNull Consumer<List<CommonFoldingFeature>> callback, + @NonNull String displayFeaturesString) { + if (mDeviceStateMapper.isDeviceStateValid(state)) { + callback.accept(calculateFoldingFeature(state, displayFeaturesString)); } else { // This callback will be added to mCallbacks and removed once it runs once. - AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback = + final AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback = new AcceptOnceConsumer<>(this, callback); addDataChangedCallback(singleRunCallback); } } - /** - * Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the - * {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was - * initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}. - * Returns a boolean value of whether the device state is valid. - */ - private boolean isCurrentStateValid() { - // If the device state is not found in the map, indexOfKey returns a negative number. - return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState.getIdentifier()) >= 0; - } - + // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the implementation of + // addDataChangedCallback(). See https://errorprone.info/bugpattern/GuardedBy for limitations. + @SuppressWarnings("GuardedBy") @Override protected void onListenersChanged() { super.onListenersChanged(); - if (hasListeners()) { - mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange); - } else { - mCurrentDeviceState = new DeviceState( - new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER, - "INVALID").build()); - mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange); + synchronized (mCurrentDeviceStateLock) { + if (hasListeners()) { + mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChangeLocked); + } else { + mCurrentDeviceState = INVALID_DEVICE_STATE; + mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChangeLocked); + } + } + } + + @NonNull + private DeviceState getCurrentDeviceState() { + synchronized (mCurrentDeviceStateLock) { + return mCurrentDeviceState; } } @NonNull @Override public Optional<List<CommonFoldingFeature>> getCurrentData() { - Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData(); - if (!isCurrentStateValid()) { + final Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData(); + final DeviceState state = getCurrentDeviceState(); + if (!mDeviceStateMapper.isDeviceStateValid(state) || displayFeaturesString.isEmpty()) { return Optional.empty(); } else { - return displayFeaturesString.map(this::calculateFoldingFeature); + return Optional.of(calculateFoldingFeature(state, displayFeaturesString.get())); } } @@ -191,7 +181,7 @@ public final class DeviceStateManagerFoldingFeatureProducer */ @NonNull public List<CommonFoldingFeature> getFoldsWithUnknownState() { - Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData(); + final Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData(); if (optionalFoldingFeatureString.isPresent()) { return CommonFoldingFeature.parseListFromString( @@ -201,7 +191,6 @@ public final class DeviceStateManagerFoldingFeatureProducer return Collections.emptyList(); } - /** * Returns the list of supported {@link DisplayFoldFeatureCommon} calculated from the * {@link DeviceStateManagerFoldingFeatureProducer}. @@ -218,16 +207,16 @@ public final class DeviceStateManagerFoldingFeatureProducer return foldFeatures; } - /** * Returns {@code true} if the device supports half-opened mode, {@code false} otherwise. */ public boolean isHalfOpenedSupported() { - return mIsHalfOpenedSupported; + return mDeviceStateMapper.mIsHalfOpenedSupported; } /** * Adds the data to the storeFeaturesConsumer when the data is ready. + * * @param storeFeaturesConsumer a consumer to collect the data when it is first available. */ @Override @@ -236,38 +225,123 @@ public final class DeviceStateManagerFoldingFeatureProducer if (TextUtils.isEmpty(displayFeaturesString)) { storeFeaturesConsumer.accept(new ArrayList<>()); } else { - runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString); + final DeviceState state = getCurrentDeviceState(); + runCallbackWhenValidState(state, storeFeaturesConsumer, displayFeaturesString); } }); } - private void notifyFoldingFeatureChange(String displayFeaturesString) { - if (!isCurrentStateValid()) { + @GuardedBy("mCurrentDeviceStateLock") + private void notifyFoldingFeatureChangeLocked(String displayFeaturesString) { + final DeviceState state = mCurrentDeviceState; + if (!mDeviceStateMapper.isDeviceStateValid(state)) { return; } if (TextUtils.isEmpty(displayFeaturesString)) { notifyDataChanged(new ArrayList<>()); } else { - notifyDataChanged(calculateFoldingFeature(displayFeaturesString)); + notifyDataChanged(calculateFoldingFeature(state, displayFeaturesString)); } } - private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) { - return parseListFromString(displayFeaturesString, currentHingeState()); + @NonNull + private List<CommonFoldingFeature> calculateFoldingFeature(@NonNull DeviceState deviceState, + @NonNull String displayFeaturesString) { + @CommonFoldingFeature.State + final int hingeState = mDeviceStateMapper.getHingeState(deviceState); + return parseListFromString(displayFeaturesString, hingeState); } - @CommonFoldingFeature.State - private int currentHingeState() { - @CommonFoldingFeature.State - int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState.getIdentifier(), - COMMON_STATE_UNKNOWN); + /** + * Internal class to map device states to corresponding postures. + * + * <p>This class encapsulates the logic for mapping device states to postures. The mapping is + * immutable after initialization to ensure thread safety. + */ + private static class DeviceStateMapper { + /** + * Emulated device state + * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to + * {@link CommonFoldingFeature.State} map. + * + * <p>This map must be immutable after initialization to ensure thread safety, as it may be + * accessed from multiple threads. Modifications should only occur during object + * construction. + */ + private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray(); + + /** + * The list of device states that are supported. + * + * <p>This list must be immutable after initialization to ensure thread safety. + */ + @NonNull + private final List<DeviceState> mSupportedStates; + + final boolean mIsHalfOpenedSupported; + + DeviceStateMapper(@NonNull Context context, @NonNull List<DeviceState> supportedStates) { + mSupportedStates = supportedStates; + + final String[] deviceStatePosturePairs = context.getResources() + .getStringArray(R.array.config_device_state_postures); + boolean isHalfOpenedSupported = false; + for (String deviceStatePosturePair : deviceStatePosturePairs) { + final String[] deviceStatePostureMapping = deviceStatePosturePair.split(":"); + if (deviceStatePostureMapping.length != 2) { + if (DEBUG) { + Log.e(TAG, "Malformed device state posture pair: " + + deviceStatePosturePair); + } + continue; + } - if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) { - posture = mDeviceStateToPostureMap.get( - DeviceStateUtil.calculateBaseStateIdentifier(mCurrentDeviceState, - mSupportedStates), COMMON_STATE_UNKNOWN); + final int deviceState; + final int posture; + try { + deviceState = Integer.parseInt(deviceStatePostureMapping[0]); + posture = Integer.parseInt(deviceStatePostureMapping[1]); + } catch (NumberFormatException e) { + if (DEBUG) { + Log.e(TAG, "Failed to parse device state or posture: " + + deviceStatePosturePair, + e); + } + continue; + } + isHalfOpenedSupported = isHalfOpenedSupported + || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED; + mDeviceStateToPostureMap.put(deviceState, posture); + } + mIsHalfOpenedSupported = isHalfOpenedSupported; + } + + boolean isDeviceStateToPostureMapEmpty() { + return mDeviceStateToPostureMap.size() == 0; + } + + /** + * Validates if the provided deviceState exists in the {@link #mDeviceStateToPostureMap} + * which was initialized in the constructor of {@link DeviceStateMapper}. + * Returns a boolean value of whether the device state is valid. + */ + boolean isDeviceStateValid(@NonNull DeviceState deviceState) { + // If the device state is not found in the map, indexOfKey returns a negative number. + return mDeviceStateToPostureMap.indexOfKey(deviceState.getIdentifier()) >= 0; } - return posture; + @CommonFoldingFeature.State + int getHingeState(@NonNull DeviceState deviceState) { + @CommonFoldingFeature.State + final int posture = + mDeviceStateToPostureMap.get(deviceState.getIdentifier(), COMMON_STATE_UNKNOWN); + if (posture != CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) { + return posture; + } + + final int baseStateIdentifier = + DeviceStateUtil.calculateBaseStateIdentifier(deviceState, mSupportedStates); + return mDeviceStateToPostureMap.get(baseStateIdentifier, COMMON_STATE_UNKNOWN); + } } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java index bfccb29bc952..e3a1d8ac48e2 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java @@ -142,6 +142,19 @@ class BackupHelper { } } + void abortTaskContainerRebuilding(@NonNull WindowContainerTransaction wct) { + // Clean-up the legacy states in the system + for (int i = mTaskFragmentInfos.size() - 1; i >= 0; i--) { + final TaskFragmentInfo info = mTaskFragmentInfos.valueAt(i); + mPresenter.deleteTaskFragment(wct, info.getFragmentToken()); + } + mPresenter.setSavedState(new Bundle()); + + mParcelableTaskContainerDataList.clear(); + mTaskFragmentInfos.clear(); + mTaskFragmentParentInfos.clear(); + } + boolean hasPendingStateToRestore() { return !mParcelableTaskContainerDataList.isEmpty(); } @@ -196,6 +209,7 @@ class BackupHelper { mController.onTaskFragmentParentRestored(wct, taskContainer.getTaskId(), mTaskFragmentParentInfos.get(taskContainer.getTaskId())); + mTaskFragmentParentInfos.remove(taskContainer.getTaskId()); restoredAny = true; } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index db4bb0e5e75e..8345b409ae52 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -56,6 +56,7 @@ import static androidx.window.extensions.embedding.TaskFragmentContainer.Overlay import android.annotation.CallbackExecutor; import android.app.Activity; import android.app.ActivityClient; +import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.AppGlobals; @@ -280,7 +281,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen mSplitRules.clear(); mSplitRules.addAll(rules); - if (!Flags.aeBackStackRestore() || !mPresenter.isRebuildTaskContainersNeeded()) { + if (!Flags.aeBackStackRestore() || !mPresenter.isWaitingToRebuildTaskContainers()) { return; } @@ -2893,6 +2894,36 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return; } synchronized (mLock) { + if (mPresenter.isWaitingToRebuildTaskContainers()) { + Log.w(TAG, "Rebuilding aborted, clean up and restart"); + + // Retrieve the Task intent. + final int taskId = getTaskId(activity); + Intent taskIntent = null; + final ActivityManager am = activity.getSystemService(ActivityManager.class); + final List<ActivityManager.AppTask> appTasks = am.getAppTasks(); + for (ActivityManager.AppTask appTask : appTasks) { + if (appTask.getTaskInfo().taskId == taskId) { + taskIntent = appTask.getTaskInfo().baseIntent.cloneFilter(); + break; + } + } + + // Clean up and abort the restoration + // TODO(b/369488857): also to remove the non-organized activities in the Task? + final TransactionRecord transactionRecord = + mTransactionManager.startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + mPresenter.abortTaskContainerRebuilding(wct); + transactionRecord.apply(false /* shouldApplyIndependently */); + + // Start the Task root activity. + if (taskIntent != null) { + activity.startActivity(taskIntent); + } + return; + } + final IBinder activityToken = activity.getActivityToken(); final IBinder initialTaskFragmentToken = getTaskFragmentTokenFromActivityClientRecord(activity); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 0c0ded9bad74..b498ee2ff438 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -187,10 +187,14 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { mBackupHelper.scheduleBackup(); } - boolean isRebuildTaskContainersNeeded() { + boolean isWaitingToRebuildTaskContainers() { return mBackupHelper.hasPendingStateToRestore(); } + void abortTaskContainerRebuilding(@NonNull WindowContainerTransaction wct) { + mBackupHelper.abortTaskContainerRebuilding(wct); + } + boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct, @NonNull Set<EmbeddingRule> rules) { return mBackupHelper.rebuildTaskContainers(wct, rules); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 74cce68f270b..b453f1d4e936 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -156,7 +156,7 @@ class TaskContainer { mSplitController = splitController; for (ParcelableTaskFragmentContainerData tfData : data.getParcelableTaskFragmentContainerDataList()) { - final TaskFragmentInfo info = taskFragmentInfoMap.get(tfData.mToken); + final TaskFragmentInfo info = taskFragmentInfoMap.remove(tfData.mToken); if (info != null && !info.isEmpty()) { final TaskFragmentContainer container = new TaskFragmentContainer(tfData, splitController, this); @@ -377,8 +377,16 @@ class TaskContainer { @Nullable TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { - return getContainer(container -> container.hasAppearedActivity(activityToken) - || container.hasPendingAppearedActivity(activityToken)); + // When the new activity is launched to the topmost TF because the source activity + // was in that TF, and the source activity is finished before resolving the new activity, + // we will try to see if the new activity match a rule with the split activities below. + // If matched, it can be reparented. + final TaskFragmentContainer taskFragmentContainer + = getContainer(container -> container.hasPendingAppearedActivity(activityToken)); + if (taskFragmentContainer != null) { + return taskFragmentContainer; + } + return getContainer(container -> container.hasAppearedActivity(activityToken)); } @Nullable diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp index bd430c0e610b..09185ee203b8 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/Android.bp +++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp @@ -29,6 +29,7 @@ android_test { srcs: [ "**/*.java", + "**/*.kt", ], static_libs: [ @@ -41,6 +42,7 @@ android_test { "androidx.test.ext.junit", "flag-junit", "mockito-target-extended-minus-junit4", + "mockito-kotlin-nodeps", "truth", "testables", "platform-test-annotations", diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt new file mode 100644 index 000000000000..90887a747a6f --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.common + +import android.content.Context +import android.content.res.Resources +import android.hardware.devicestate.DeviceState +import android.hardware.devicestate.DeviceStateManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.window.common.layout.CommonFoldingFeature +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_FLAT +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_HALF_OPENED +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_NO_FOLDING_FEATURES +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE +import androidx.window.common.layout.DisplayFoldFeatureCommon +import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED +import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN +import com.android.internal.R +import com.android.window.flags.Flags +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import java.util.concurrent.Executor +import java.util.function.Consumer +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify + +/** + * Test class for [DeviceStateManagerFoldingFeatureProducer]. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:DeviceStateManagerFoldingFeatureProducerTest + */ +@RunWith(AndroidJUnit4::class) +class DeviceStateManagerFoldingFeatureProducerTest { + @get:Rule + val setFlagsRule: SetFlagsRule = SetFlagsRule() + + private val mMockDeviceStateManager = mock<DeviceStateManager>() + private val mMockResources = mock<Resources> { + on { getStringArray(R.array.config_device_state_postures) } doReturn DEVICE_STATE_POSTURES + } + private val mMockContext = mock<Context> { + on { resources } doReturn mMockResources + } + private val mRawFoldSupplier = mock<RawFoldingFeatureProducer> { + on { currentData } doReturn Optional.of(DISPLAY_FEATURES) + on { getData(any<Consumer<String>>()) } doAnswer { invocation -> + val callback = invocation.getArgument(0) as Consumer<String> + callback.accept(DISPLAY_FEATURES) + } + } + + @Test + @DisableFlags(Flags.FLAG_WLINFO_ONCREATE) + fun testRegisterCallback_whenWlinfoOncreateIsDisabled_usesMainExecutor() { + DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + verify(mMockDeviceStateManager).registerCallback(eq(mMockContext.mainExecutor), any()) + } + + @Test + @EnableFlags(Flags.FLAG_WLINFO_ONCREATE) + fun testRegisterCallback_whenWlinfoOncreateIsEnabled_usesRunnableRun() { + val executorCaptor = ArgumentCaptor.forClass(Executor::class.java) + val runnable = mock<Runnable>() + + DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + verify(mMockDeviceStateManager).registerCallback(executorCaptor.capture(), any()) + executorCaptor.value.execute(runnable) + verify(runnable).run() + } + + @Test + fun testGetCurrentData_validCurrentState_returnsFoldingFeatureWithState() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED) + + val currentData = ffp.getCurrentData() + + assertThat(currentData).isPresent() + assertThat(currentData.get()).containsExactlyElementsIn(HALF_OPENED_FOLDING_FEATURES) + } + + @Test + fun testGetCurrentData_invalidCurrentState_returnsEmptyOptionalFoldingFeature() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + val currentData = ffp.getCurrentData() + + assertThat(currentData).isEmpty() + } + + @Test + fun testGetFoldsWithUnknownState_validFoldingFeature_returnsFoldingFeaturesWithUnknownState() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + val result = ffp.getFoldsWithUnknownState() + + assertThat(result).containsExactlyElementsIn(UNKNOWN_STATE_FOLDING_FEATURES) + } + + @Test + fun testGetFoldsWithUnknownState_emptyFoldingFeature_returnsEmptyList() { + mRawFoldSupplier.stub { + on { currentData } doReturn Optional.empty() + } + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + val result = ffp.getFoldsWithUnknownState() + + assertThat(result).isEmpty() + } + + @Test + fun testGetDisplayFeatures_validFoldingFeature_returnsDisplayFoldFeatures() { + mRawFoldSupplier.stub { + on { currentData } doReturn Optional.of(DISPLAY_FEATURES_HALF_OPENED_HINGE) + } + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + val result = ffp.displayFeatures + + assertThat(result).containsExactly( + DisplayFoldFeatureCommon( + DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN, + setOf(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED), + ), + ) + } + + @Test + fun testIsHalfOpenedSupported_withHalfOpenedPostures_returnsTrue() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + assertThat(ffp.isHalfOpenedSupported).isTrue() + } + + @Test + fun testIsHalfOpenedSupported_withEmptyPostures_returnsFalse() { + mMockResources.stub { + on { getStringArray(R.array.config_device_state_postures) } doReturn emptyArray() + } + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + assertThat(ffp.isHalfOpenedSupported).isFalse() + } + + @Test + fun testGetData_emptyDisplayFeaturesString_callsConsumerWithEmptyList() { + mRawFoldSupplier.stub { + on { getData(any<Consumer<String>>()) } doAnswer { invocation -> + val callback = invocation.getArgument(0) as Consumer<String> + callback.accept("") + } + } + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>() + + ffp.getData(storeFeaturesConsumer) + + verify(storeFeaturesConsumer).accept(emptyList()) + } + + @Test + fun testGetData_validState_callsConsumerWithFoldingFeatures() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED) + val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>() + + ffp.getData(storeFeaturesConsumer) + + verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES) + } + + @Test + fun testGetData_invalidState_addsAcceptOnceConsumerToDataChangedCallback() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>() + + ffp.getData(storeFeaturesConsumer) + + verify(storeFeaturesConsumer, never()).accept(any()) + ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED) + ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_OPENED) + verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES) + } + + @Test + fun testDeviceStateMapper_malformedDeviceStatePosturePair_skipsPair() { + val malformedDeviceStatePostures = arrayOf( + // Missing the posture. + "0", + // Empty string. + "", + // Too many elements. + "0:1:2", + ) + mMockResources.stub { + on { getStringArray(R.array.config_device_state_postures) } doReturn + malformedDeviceStatePostures + } + + DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + verify(mMockDeviceStateManager, never()).registerCallback(any(), any()) + } + + @Test + fun testDeviceStateMapper_invalidNumberFormat_skipsPair() { + val invalidNumberFormatDeviceStatePostures = arrayOf("a:1", "0:b", "a:b", ":1") + mMockResources.stub { + on { getStringArray(R.array.config_device_state_postures) } doReturn + invalidNumberFormatDeviceStatePostures + } + + DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + verify(mMockDeviceStateManager, never()).registerCallback(any(), any()) + } + + companion object { + // Supported device states configuration. + private enum class SupportedDeviceStates { + CLOSED, HALF_OPENED, OPENED, REAR_DISPLAY, CONCURRENT; + + override fun toString() = ordinal.toString() + + fun toDeviceState(): DeviceState = + DeviceState(DeviceState.Configuration.Builder(ordinal, name).build()) + } + + // Map of supported device states supplied by DeviceStateManager to WM Jetpack posture. + private val DEVICE_STATE_POSTURES = + arrayOf( + "${SupportedDeviceStates.CLOSED}:$COMMON_STATE_NO_FOLDING_FEATURES", + "${SupportedDeviceStates.HALF_OPENED}:$COMMON_STATE_HALF_OPENED", + "${SupportedDeviceStates.OPENED}:$COMMON_STATE_FLAT", + "${SupportedDeviceStates.REAR_DISPLAY}:$COMMON_STATE_NO_FOLDING_FEATURES", + "${SupportedDeviceStates.CONCURRENT}:$COMMON_STATE_USE_BASE_STATE", + ) + private val DEVICE_STATE_HALF_OPENED = SupportedDeviceStates.HALF_OPENED.toDeviceState() + private val DEVICE_STATE_OPENED = SupportedDeviceStates.OPENED.toDeviceState() + + // WindowsManager Jetpack display features. + private val DISPLAY_FEATURES = "fold-[1104,0,1104,1848]" + private val DISPLAY_FEATURES_HALF_OPENED_HINGE = "$DISPLAY_FEATURES-half-opened" + private val HALF_OPENED_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString( + DISPLAY_FEATURES, + COMMON_STATE_HALF_OPENED, + ) + private val UNKNOWN_STATE_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString( + DISPLAY_FEATURES, + COMMON_STATE_UNKNOWN, + ) + } +} diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index 7fab371cb790..bc4916a607a3 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -535,7 +535,8 @@ public class TaskFragmentContainerTest { // container1. container2.setInfo(mTransaction, mInfo); - assertTrue(container2.hasActivity(mActivity.getActivityToken())); + assertTrue(container1.hasActivity(mActivity.getActivityToken())); + assertFalse(container2.hasActivity(mActivity.getActivityToken())); // When the pending appeared record is removed from container1, we respect the appeared // record in container2. container1.removePendingAppearedActivity(mActivity.getActivityToken()); diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 63a288079401..cf0a975b6c30 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -156,6 +156,13 @@ flag { } flag { + name: "enable_flexible_two_app_split" + namespace: "multitasking" + description: "Enables only 2 app 90:10 split" + bug: "349828130" +} + +flag { name: "enable_flexible_split" namespace: "multitasking" description: "Enables flexibile split feature for split screen" diff --git a/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml b/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml new file mode 100644 index 000000000000..07e5ac1a604b --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?android:attr/textColorTertiary" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/system_on_tertiary_fixed" + android:pathData="M419,880Q391,880 366.5,868Q342,856 325,834L107,557L126,537Q146,516 174,512Q202,508 226,523L300,568L300,240Q300,223 311.5,211.5Q323,200 340,200Q357,200 369,211.5Q381,223 381,240L381,712L284,652L388,785Q394,792 402,796Q410,800 419,800L640,800Q673,800 696.5,776.5Q720,753 720,720L720,560Q720,543 708.5,531.5Q697,520 680,520L461,520L461,440L680,440Q730,440 765,475Q800,510 800,560L800,720Q800,786 753,833Q706,880 640,880L419,880ZM167,340Q154,318 147,292.5Q140,267 140,240Q140,157 198.5,98.5Q257,40 340,40Q423,40 481.5,98.5Q540,157 540,240Q540,267 533,292.5Q526,318 513,340L444,300Q452,286 456,271.5Q460,257 460,240Q460,190 425,155Q390,120 340,120Q290,120 255,155Q220,190 220,240Q220,257 224,271.5Q228,286 236,300L167,340ZM502,620L502,620L502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620L502,620Z" /> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml new file mode 100644 index 000000000000..a12a74658953 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <shape android:shape="rectangle"> + <corners android:radius="30dp" /> + <solid android:color="@android:color/system_tertiary_fixed" /> + </shape> + </item> +</layer-list> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml new file mode 100644 index 000000000000..aadffb5a0003 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- An arrow that points towards left. --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="10dp" + android:height="12dp" + android:viewportWidth="10" + android:viewportHeight="12"> + <path + android:pathData="M2.858,4.285C1.564,5.062 1.564,6.938 2.858,7.715L10,12L10,0L2.858,4.285Z" + android:fillColor="@android:color/system_tertiary_fixed"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml new file mode 100644 index 000000000000..e3c9a662671e --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml @@ -0,0 +1,26 @@ +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- An arrow that points upwards. --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="12dp" + android:height="9dp" + android:viewportWidth="12" + android:viewportHeight="9"> + <path + android:pathData="M7.715,1.858C6.938,0.564 5.062,0.564 4.285,1.858L0,9L12,9L7.715,1.858Z" + android:fillColor="@android:color/system_tertiary_fixed"/> +</vector> diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml new file mode 100644 index 000000000000..a269b9ee1dd5 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:elevation="1dp" + android:orientation="horizontal"> + + <!-- ImageView for the arrow icon, positioned horizontally at the start of the tooltip + container. --> + <ImageView + android:id="@+id/arrow_icon" + android:layout_width="10dp" + android:layout_height="12dp" + android:layout_gravity="center_vertical" + android:src="@drawable/desktop_windowing_education_tooltip_left_arrow" /> + + <!-- Layout for the tooltip, excluding the arrow. Separating the tooltip content from the arrow + allows scaling of only the tooltip container when the content changes, without affecting the + arrow. --> + <include layout="@layout/desktop_windowing_education_tooltip_container" /> +</LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml new file mode 100644 index 000000000000..bdee8836dc2e --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/tooltip_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/desktop_windowing_education_tooltip_background" + android:orientation="horizontal" + android:padding="@dimen/desktop_windowing_education_tooltip_padding"> + + <ImageView + android:id="@+id/tooltip_icon" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_gravity="center_vertical" + android:src="@drawable/app_handle_education_tooltip_icon" /> + + <TextView + android:id="@+id/tooltip_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginStart="2dp" + android:lineHeight="20dp" + android:maxWidth="150dp" + android:textColor="@android:color/system_on_tertiary_fixed" + android:textFontWeight="500" + android:textSize="14sp" /> +</LinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml new file mode 100644 index 000000000000..c73c1dad0e18 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:elevation="1dp" + android:orientation="vertical"> + + <!-- ImageView for the arrow icon, positioned vertically above the tooltip container. --> + <ImageView + android:id="@+id/arrow_icon" + android:layout_width="12dp" + android:layout_height="9dp" + android:layout_gravity="center_horizontal" + android:src="@drawable/desktop_windowing_education_tooltip_top_arrow" /> + + <!-- Layout for the tooltip, excluding the arrow. Separating the tooltip content from the arrow + allows scaling of only the tooltip container when the content changes, without affecting the + arrow. --> + <include layout="@layout/desktop_windowing_education_tooltip_container" /> +</LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml index 045b975a854e..462a49ccb1eb 100644 --- a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml +++ b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml @@ -99,11 +99,11 @@ </LinearLayout> - <FrameLayout + + <LinearLayout android:minHeight="@dimen/letterbox_restart_dialog_button_height" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" - style="?android:attr/buttonBarButtonStyle" android:layout_gravity="end"> <Button @@ -133,7 +133,7 @@ android:text="@string/letterbox_restart_restart" android:contentDescription="@string/letterbox_restart_restart"/> - </FrameLayout> + </LinearLayout> </LinearLayout> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 3d8718332199..c7109f5be132 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -608,6 +608,9 @@ <!-- The horizontal inset to apply to the close button's ripple drawable --> <dimen name="desktop_mode_header_close_ripple_inset_horizontal">6dp</dimen> + <!-- The padding added to all sides of windowing education tooltip --> + <dimen name="desktop_windowing_education_tooltip_padding">8dp</dimen> + <!-- The acceptable area ratio of fg icon area/bg icon area, i.e. (72 x 72) / (108 x 108) --> <item type="dimen" format="float" name="splash_icon_enlarge_foreground_threshold">0.44</item> <!-- Scaling factor applied to splash icons without provided background i.e. (192 / 160) --> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index bda56860d3ba..56f25dae3df2 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -219,6 +219,15 @@ compatibility control. [CHAR LIMIT=NONE] --> <string name="camera_compat_dismiss_button_description">No camera issues? Tap to dismiss.</string> + <!-- App handle education tooltip text for tooltip pointing to app handle --> + <string name="windowing_app_handle_education_tooltip">Tap to open the app menu</string> + + <!-- App handle education tooltip text for tooltip pointing to windowing image button --> + <string name="windowing_desktop_mode_image_button_education_tooltip">Tap to show multiple apps together</string> + + <!-- App handle education tooltip text for tooltip pointing to app chip --> + <string name="windowing_desktop_mode_exit_education_tooltip">Return to fullscreen from the app menu</string> + <!-- The title of the letterbox education dialog. [CHAR LIMIT=NONE] --> <string name="letterbox_education_dialog_title">See and do more</string> @@ -307,12 +316,11 @@ <!-- Maximize menu snap buttons string. --> <string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string> <!-- Snap resizing non-resizable string. --> - <string name="desktop_mode_non_resizable_snap_text">This app can\'t be resized</string> + <string name="desktop_mode_non_resizable_snap_text">App can\'t be moved here</string> <!-- Accessibility text for the Maximize Menu's maximize button [CHAR LIMIT=NONE] --> <string name="desktop_mode_maximize_menu_maximize_button_text">Maximize</string> <!-- Accessibility text for the Maximize Menu's snap left button [CHAR LIMIT=NONE] --> <string name="desktop_mode_maximize_menu_snap_left_button_text">Snap left</string> <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] --> <string name="desktop_mode_maximize_menu_snap_right_button_text">Snap right</string> - </resources> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java new file mode 100644 index 000000000000..26aae2d2aa78 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared; + +import com.android.wm.shell.shared.annotations.ExternalThread; + +/** + * Listener to get focus-related transition callbacks. + */ +@ExternalThread +public interface FocusTransitionListener { + /** + * Called when a transition changes the top, focused display. + */ + void onFocusedDisplayChanged(int displayId); +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl new file mode 100644 index 000000000000..b91d5b6e2769 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared; + +/** + * Listener interface that to get focus-related transition callbacks. + */ +oneway interface IFocusTransitionListener { + + /** + * Called when a transition changes the top, focused display. + */ + void onFocusedDisplayChanged(int displayId); +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl index 3256abf09116..02615a96a86c 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl @@ -20,6 +20,7 @@ import android.view.SurfaceControl; import android.window.RemoteTransition; import android.window.TransitionFilter; +import com.android.wm.shell.shared.IFocusTransitionListener; import com.android.wm.shell.shared.IHomeTransitionListener; /** @@ -59,4 +60,9 @@ interface IShellTransitions { */ oneway void registerRemoteForTakeover(in TransitionFilter filter, in RemoteTransition remoteTransition) = 6; + + /** + * Set listener that will receive callbacks about transitions involving focus switch. + */ + oneway void setFocusTransitionListener(in IFocusTransitionListener listener) = 7; } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java index 6d4ab4c1bd09..2db4311fb771 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java @@ -22,6 +22,8 @@ import android.window.TransitionFilter; import com.android.wm.shell.shared.annotations.ExternalThread; +import java.util.concurrent.Executor; + /** * Interface to manage remote transitions. */ @@ -44,4 +46,15 @@ public interface ShellTransitions { * Unregisters a remote transition for all operations. */ default void unregisterRemote(@NonNull RemoteTransition remoteTransition) {} + + /** + * Sets listener that will receive callbacks about transitions involving focus switch. + */ + default void setFocusTransitionListener(@NonNull FocusTransitionListener listener, + Executor executor) {} + + /** + * Unsets listener that will receive callbacks about transitions involving focus switch. + */ + default void unsetFocusTransitionListener(@NonNull FocusTransitionListener listener) {} } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS new file mode 100644 index 000000000000..bfb6d4ac5849 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS @@ -0,0 +1,4 @@ +jeremysim@google.com +winsonc@google.com +peanutbutter@google.com +shuminghao@google.com diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java index 498dc8bdd24d..7f1e4a873f64 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java @@ -66,14 +66,54 @@ public class SplitScreenConstants { public @interface SplitPosition { } - /** A snap target in the first half of the screen, where the split is roughly 30-70. */ - public static final int SNAP_TO_30_70 = 0; + /** + * A snap target for two apps, where the split is 33-66. With FLAG_ENABLE_FLEXIBLE_SPLIT, + * only used on tablets. + */ + public static final int SNAP_TO_2_33_66 = 0; + + /** A snap target for two apps, where the split is 50-50. */ + public static final int SNAP_TO_2_50_50 = 1; + + /** + * A snap target for two apps, where the split is 66-33. With FLAG_ENABLE_FLEXIBLE_SPLIT, + * only used on tablets. + */ + public static final int SNAP_TO_2_66_33 = 2; - /** The 50-50 snap target */ - public static final int SNAP_TO_50_50 = 1; + /** + * A snap target for two apps, where the split is 90-10. The "10" app extends off the screen, + * and is actually the same size as the onscreen app, but the visible portion takes up 10% of + * the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, used on phones and foldables. + */ + public static final int SNAP_TO_2_90_10 = 3; - /** A snap target in the latter half of the screen, where the split is roughly 70-30. */ - public static final int SNAP_TO_70_30 = 2; + /** + * A snap target for two apps, where the split is 10-90. The "10" app extends off the screen, + * and is actually the same size as the onscreen app, but the visible portion takes up 10% of + * the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, used on phones and foldables. + */ + public static final int SNAP_TO_2_10_90 = 4; + + /** + * A snap target for three apps, where the split is 33-33-33. With FLAG_ENABLE_FLEXIBLE_SPLIT, + * only used on tablets. + */ + public static final int SNAP_TO_3_33_33_33 = 5; + + /** + * A snap target for three apps, where the split is 45-45-10. The "10" app extends off the + * screen, and is actually the same size as the onscreen apps, but the visible portion takes + * up 10% of the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, only used on unfolded foldables. + */ + public static final int SNAP_TO_3_45_45_10 = 6; + + /** + * A snap target for three apps, where the split is 10-45-45. The "10" app extends off the + * screen, and is actually the same size as the onscreen apps, but the visible portion takes + * up 10% of the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, only used on unfolded foldables. + */ + public static final int SNAP_TO_3_10_45_45 = 7; /** * These snap targets are used for split pairs in a stable, non-transient state. They may be @@ -81,9 +121,14 @@ public class SplitScreenConstants { * {@link SnapPosition}. */ @IntDef(prefix = { "SNAP_TO_" }, value = { - SNAP_TO_30_70, - SNAP_TO_50_50, - SNAP_TO_70_30 + SNAP_TO_2_33_66, + SNAP_TO_2_50_50, + SNAP_TO_2_66_33, + SNAP_TO_2_90_10, + SNAP_TO_2_10_90, + SNAP_TO_3_33_33_33, + SNAP_TO_3_45_45_10, + SNAP_TO_3_10_45_45, }) public @interface PersistentSnapPosition {} @@ -91,9 +136,14 @@ public class SplitScreenConstants { * Checks if the snapPosition in question is a {@link PersistentSnapPosition}. */ public static boolean isPersistentSnapPosition(@SnapPosition int snapPosition) { - return snapPosition == SNAP_TO_30_70 - || snapPosition == SNAP_TO_50_50 - || snapPosition == SNAP_TO_70_30; + return snapPosition == SNAP_TO_2_33_66 + || snapPosition == SNAP_TO_2_50_50 + || snapPosition == SNAP_TO_2_66_33 + || snapPosition == SNAP_TO_2_90_10 + || snapPosition == SNAP_TO_2_10_90 + || snapPosition == SNAP_TO_3_33_33_33 + || snapPosition == SNAP_TO_3_45_45_10 + || snapPosition == SNAP_TO_3_10_45_45; } /** The divider doesn't snap to any target and is freely placeable. */ @@ -109,9 +159,14 @@ public class SplitScreenConstants { public static final int SNAP_TO_MINIMIZE = 13; @IntDef(prefix = { "SNAP_TO_" }, value = { - SNAP_TO_30_70, - SNAP_TO_50_50, - SNAP_TO_70_30, + SNAP_TO_2_33_66, + SNAP_TO_2_50_50, + SNAP_TO_2_66_33, + SNAP_TO_2_90_10, + SNAP_TO_2_10_90, + SNAP_TO_3_33_33_33, + SNAP_TO_3_45_45_10, + SNAP_TO_3_10_45_45, SNAP_TO_NONE, SNAP_TO_START_AND_DISMISS, SNAP_TO_END_AND_DISMISS, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java index f7f45ae36eda..9f100facc163 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java @@ -19,9 +19,9 @@ package com.android.wm.shell.common.split; import static android.view.WindowManager.DOCKED_LEFT; import static android.view.WindowManager.DOCKED_RIGHT; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_30_70; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_70_30; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_MINIMIZE; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE; @@ -283,10 +283,10 @@ public class DividerSnapAlgorithm { private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition, int bottomPosition, int dividerMax) { - maybeAddTarget(topPosition, topPosition - getStartInset(), SNAP_TO_30_70); + maybeAddTarget(topPosition, topPosition - getStartInset(), SNAP_TO_2_33_66); addMiddleTarget(isHorizontalDivision); maybeAddTarget(bottomPosition, - dividerMax - getEndInset() - (bottomPosition + mDividerSize), SNAP_TO_70_30); + dividerMax - getEndInset() - (bottomPosition + mDividerSize), SNAP_TO_2_66_33); } private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) { @@ -332,7 +332,7 @@ public class DividerSnapAlgorithm { private void addMiddleTarget(boolean isHorizontalDivision) { int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision, mInsets, mDisplayWidth, mDisplayHeight, mDividerSize); - mTargets.add(new SnapTarget(position, SNAP_TO_50_50)); + mTargets.add(new SnapTarget(position, SNAP_TO_2_50_50)); } private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index bec2ea58e106..4227a6e2903f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -123,6 +123,7 @@ import com.android.wm.shell.sysui.ShellInterface; import com.android.wm.shell.taskview.TaskViewFactory; import com.android.wm.shell.taskview.TaskViewFactoryController; import com.android.wm.shell.taskview.TaskViewTransitions; +import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.MixedTransitionHandler; import com.android.wm.shell.transition.Transitions; @@ -742,14 +743,15 @@ public abstract class WMShellBaseModule { @ShellMainThread Handler mainHandler, @ShellAnimationThread ShellExecutor animExecutor, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - HomeTransitionObserver homeTransitionObserver) { + HomeTransitionObserver homeTransitionObserver, + FocusTransitionObserver focusTransitionObserver) { if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) { // TODO(b/238217847): Force override shell init if registration is disabled shellInit = new ShellInit(mainExecutor); } return new Transitions(context, shellInit, shellCommandHandler, shellController, organizer, pool, displayController, mainExecutor, mainHandler, animExecutor, - rootTaskDisplayAreaOrganizer, homeTransitionObserver); + rootTaskDisplayAreaOrganizer, homeTransitionObserver, focusTransitionObserver); } @WMSingleton @@ -761,6 +763,12 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides + static FocusTransitionObserver provideFocusTransitionObserver() { + return new FocusTransitionObserver(); + } + + @WMSingleton + @Provides static TaskViewTransitions provideTaskViewTransitions(Transitions transitions) { return new TaskViewTransitions(transitions); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 759ed035895e..0e8c4e70e05d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -456,6 +456,7 @@ class DesktopModeTaskRepository ( pw.println( "${innerPrefix}freeformTasksInZOrder=${data.freeformTasksInZOrder.toDumpString()}" ) + pw.println("${innerPrefix}minimizedTasks=${data.minimizedTasks.toDumpString()}") } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 968f40c3df5d..afa27f9f1309 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -43,6 +43,7 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.DragEvent import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE +import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_NONE import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_FRONT @@ -1061,7 +1062,10 @@ class DesktopTasksController( // Check if freeform task launch during recents should be handled shouldHandleMidRecentsFreeformLaunch -> handleMidRecentsFreeformTaskLaunch(task) // Check if the closing task needs to be handled - TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task) + TransitionUtil.isClosingType(request.type) -> handleTaskClosing( + task, + request.type + ) // Check if the top task shouldn't be allowed to enter desktop mode isIncompatibleTask(task) -> handleIncompatibleTaskLaunch(task) // Check if fullscreen task should be updated @@ -1288,7 +1292,10 @@ class DesktopTasksController( } /** Handle task closing by removing wallpaper activity if it's the last active task */ - private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? { + private fun handleTaskClosing( + task: RunningTaskInfo, + transitionType: Int + ): WindowContainerTransaction? { logV("handleTaskClosing") if (!isDesktopModeShowing(task.displayId)) return null @@ -1301,9 +1308,10 @@ class DesktopTasksController( removeWallpaperActivity(wct) } taskRepository.addClosingTask(task.displayId, task.taskId) - // If a CLOSE or TO_BACK is triggered on a desktop task, remove the task. + // If a CLOSE is triggered on a desktop task, remove the task. if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() && - taskRepository.isVisibleTask(task.taskId) + taskRepository.isVisibleTask(task.taskId) && + transitionType == TRANSIT_CLOSE ) { wct.removeTask(task.token) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index 0841628853a3..4796c4d0655a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -16,16 +16,19 @@ package com.android.wm.shell.desktopmode +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.Context import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager +import android.view.WindowManager.TRANSIT_TO_BACK import android.window.TransitionInfo import android.window.WindowContainerTransaction +import android.window.flags.DesktopModeFlags +import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY import com.android.internal.protolog.ProtoLog import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE -import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions @@ -64,6 +67,30 @@ class DesktopTasksTransitionObserver( ) { // TODO: b/332682201 Update repository state updateWallpaperToken(info) + + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { + handleBackNavigation(info) + } + } + + private fun handleBackNavigation(info: TransitionInfo) { + // When default back navigation happens, transition type is TO_BACK and the change is + // TO_BACK. Mark the task going to back as minimized. + if (info.type == TRANSIT_TO_BACK) { + for (change in info.changes) { + val taskInfo = change.taskInfo + if (taskInfo == null || taskInfo.taskId == -1) { + continue + } + + if (desktopModeTaskRepository.getVisibleTaskCount(taskInfo.displayId) > 0 && + change.mode == TRANSIT_TO_BACK && + taskInfo.windowingMode == WINDOWING_MODE_FREEFORM + ) { + desktopModeTaskRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId) + } + } + } } override fun onTransitionStarting(transition: IBinder) { 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 index 2138acc51eb2..cbb08b804dfe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -1344,6 +1344,9 @@ public class PipTransition extends PipTransitionController { final SurfaceControl leash = pipChange.getLeash(); final Rect destBounds = mPipOrganizer.getCurrentOrAnimatingBounds(); final boolean isInPip = mPipTransitionState.isInPip(); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Update pip for unhandled transition, change=%s, destBounds=%s, isInPip=%b", + TAG, pipChange, destBounds, isInPip); mSurfaceTransactionHelper .crop(startTransaction, leash, destBounds) .round(startTransaction, leash, isInPip) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index d3bed59f7994..a2439a937512 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -361,8 +361,11 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final int anim = getRotationAnimationHint(change, info, mDisplayController); isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS; if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) { - startRotationAnimation(startTransaction, change, info, anim, animations, - onAnimFinish); + final int flags = wallpaperTransit != WALLPAPER_TRANSITION_NONE + && Flags.commonSurfaceAnimator() + ? ScreenRotationAnimation.FLAG_HAS_WALLPAPER : 0; + startRotationAnimation(startTransaction, change, info, anim, flags, + animations, onAnimFinish); isDisplayRotationAnimationStarted = true; continue; } @@ -414,7 +417,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { if (change.getParent() == null && !change.hasFlags(FLAG_IS_DISPLAY) && change.getStartRotation() != change.getEndRotation()) { startRotationAnimation(startTransaction, change, info, - ROTATION_ANIMATION_ROTATE, animations, onAnimFinish); + ROTATION_ANIMATION_ROTATE, 0 /* flags */, animations, onAnimFinish); continue; } } @@ -699,12 +702,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } private void startRotationAnimation(SurfaceControl.Transaction startTransaction, - TransitionInfo.Change change, TransitionInfo info, int animHint, + TransitionInfo.Change change, TransitionInfo info, int animHint, int flags, ArrayList<Animator> animations, Runnable onAnimFinish) { final int rootIdx = TransitionUtil.rootIndexFor(change, info); final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mTransactionPool, startTransaction, change, info.getRoot(rootIdx).getLeash(), - animHint); + animHint, flags); // The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real // content, and background color. The item of "animGroup" will be removed if the sub // animation is finished. Then if the list becomes empty, the rotation animation is done. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java new file mode 100644 index 000000000000..2f5059f3161c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import static android.view.Display.INVALID_DISPLAY; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; + +import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions; +import static com.android.wm.shell.transition.Transitions.TransitionObserver; + +import android.annotation.NonNull; +import android.app.ActivityManager.RunningTaskInfo; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; +import android.view.SurfaceControl; +import android.window.TransitionInfo; + +import com.android.wm.shell.shared.FocusTransitionListener; +import com.android.wm.shell.shared.IFocusTransitionListener; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * The {@link TransitionObserver} that observes for transitions involving focus switch. + * It reports transitions to callers outside of the process via {@link IFocusTransitionListener}, + * and callers within the process via {@link FocusTransitionListener}. + */ +public class FocusTransitionObserver implements TransitionObserver { + private static final String TAG = FocusTransitionObserver.class.getSimpleName(); + + private IFocusTransitionListener mRemoteListener; + private final Map<FocusTransitionListener, Executor> mLocalListeners = + new HashMap<>(); + + private int mFocusedDisplayId = INVALID_DISPLAY; + + public FocusTransitionObserver() {} + + @Override + public void onTransitionReady(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + final List<TransitionInfo.Change> changes = info.getChanges(); + for (int i = changes.size() - 1; i >= 0; i--) { + final TransitionInfo.Change change = changes.get(i); + final RunningTaskInfo task = change.getTaskInfo(); + if (task != null && task.isFocused && change.hasFlags(FLAG_MOVED_TO_TOP)) { + if (mFocusedDisplayId != task.displayId) { + mFocusedDisplayId = task.displayId; + notifyFocusedDisplayChanged(); + } + return; + } + } + } + + @Override + public void onTransitionStarting(@NonNull IBinder transition) {} + + @Override + public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {} + + @Override + public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {} + + /** + * Sets the focus transition listener that receives any transitions resulting in focus switch. + * This is for calls from outside the Shell, within the host process. + * + */ + public void setLocalFocusTransitionListener(FocusTransitionListener listener, + Executor executor) { + if (!enableDisplayFocusInShellTransitions()) { + return; + } + mLocalListeners.put(listener, executor); + executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId)); + } + + /** + * Sets the focus transition listener that receives any transitions resulting in focus switch. + * This is for calls from outside the Shell, within the host process. + * + */ + public void unsetLocalFocusTransitionListener(FocusTransitionListener listener) { + if (!enableDisplayFocusInShellTransitions()) { + return; + } + mLocalListeners.remove(listener); + } + + /** + * Sets the focus transition listener that receives any transitions resulting in focus switch. + * This is for calls from outside the host process. + */ + public void setRemoteFocusTransitionListener(Transitions transitions, + IFocusTransitionListener listener) { + if (!enableDisplayFocusInShellTransitions()) { + return; + } + mRemoteListener = listener; + notifyFocusedDisplayChangedToRemote(); + } + + /** + * Notifies the listener that display focus has changed. + */ + public void notifyFocusedDisplayChanged() { + notifyFocusedDisplayChangedToRemote(); + mLocalListeners.forEach((listener, executor) -> + executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId))); + } + + private void notifyFocusedDisplayChangedToRemote() { + if (mRemoteListener != null) { + try { + mRemoteListener.onFocusedDisplayChanged(mFocusedDisplayId); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call notifyFocusedDisplayChangedToRemote", e); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java index 5802e2ca8133..1a04997fa384 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -25,12 +25,9 @@ import static com.android.wm.shell.transition.DefaultTransitionHandler.buildSurf import static com.android.wm.shell.transition.Transitions.TAG; import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.content.Context; -import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.hardware.HardwareBuffer; @@ -38,6 +35,7 @@ import android.util.Slog; import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; +import android.view.animation.AccelerateInterpolator; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.window.ScreenCapture; @@ -74,6 +72,7 @@ import java.util.ArrayList; */ class ScreenRotationAnimation { static final int MAX_ANIMATION_DURATION = 10 * 1000; + static final int FLAG_HAS_WALLPAPER = 1; private final Context mContext; private final TransactionPool mTransactionPool; @@ -98,6 +97,12 @@ class ScreenRotationAnimation { private SurfaceControl mBackColorSurface; /** The leash using to animate screenshot layer. */ private final SurfaceControl mAnimLeash; + /** + * The container with background color for {@link #mSurfaceControl}. It is only created if + * {@link #mSurfaceControl} may be translucent. E.g. visible wallpaper with alpha < 1 (dimmed). + * That prevents flickering of alpha blending. + */ + private SurfaceControl mBackEffectSurface; // The current active animation to move from the old to the new rotated // state. Which animation is run here will depend on the old and new @@ -111,8 +116,8 @@ class ScreenRotationAnimation { /** Intensity of light/whiteness of the layout after rotation occurs. */ private float mEndLuma; - ScreenRotationAnimation(Context context, TransactionPool pool, - Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash, int animHint) { + ScreenRotationAnimation(Context context, TransactionPool pool, Transaction t, + TransitionInfo.Change change, SurfaceControl rootLeash, int animHint, int flags) { mContext = context; mTransactionPool = pool; mAnimHint = animHint; @@ -170,11 +175,20 @@ class ScreenRotationAnimation { } hardwareBuffer.close(); } + if ((flags & FLAG_HAS_WALLPAPER) != 0) { + mBackEffectSurface = new SurfaceControl.Builder() + .setCallsite("ShellRotationAnimation").setParent(rootLeash) + .setEffectLayer().setOpaque(true).setName("BackEffect").build(); + t.reparent(mSurfaceControl, mBackEffectSurface) + .setColor(mBackEffectSurface, + new float[] {mStartLuma, mStartLuma, mStartLuma}) + .show(mBackEffectSurface); + } t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE); t.show(mAnimLeash); // Crop the real content in case it contains a larger child layer, e.g. wallpaper. - t.setCrop(mSurfaceControl, new Rect(0, 0, mEndWidth, mEndHeight)); + t.setCrop(getEnterSurface(), new Rect(0, 0, mEndWidth, mEndHeight)); if (!isCustomRotate()) { mBackColorSurface = new SurfaceControl.Builder() @@ -202,6 +216,11 @@ class ScreenRotationAnimation { return mAnimHint == ROTATION_ANIMATION_CROSSFADE || mAnimHint == ROTATION_ANIMATION_JUMPCUT; } + /** Returns the surface which contains the real content to animate enter. */ + private SurfaceControl getEnterSurface() { + return mBackEffectSurface != null ? mBackEffectSurface : mSurfaceControl; + } + private void setScreenshotTransform(SurfaceControl.Transaction t) { if (mScreenshotLayer == null) { return; @@ -314,7 +333,11 @@ class ScreenRotationAnimation { } else { startDisplayRotation(animations, finishCallback, mainExecutor); startScreenshotRotationAnimation(animations, finishCallback, mainExecutor); - //startColorAnimation(mTransaction, animationScale); + if (mBackEffectSurface != null && mStartLuma > 0.1f) { + // Animate from the color of background to black for smooth alpha blending. + buildLumaAnimation(animations, mStartLuma, 0f /* endLuma */, mBackEffectSurface, + animationScale, finishCallback, mainExecutor); + } } return true; @@ -322,7 +345,7 @@ class ScreenRotationAnimation { private void startDisplayRotation(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { - buildSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback, + buildSurfaceAnimation(animations, mRotateEnterAnimation, getEnterSurface(), finishCallback, mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, null /* clipRect */, false /* isActivity */); } @@ -341,40 +364,17 @@ class ScreenRotationAnimation { null /* clipRect */, false /* isActivity */); } - private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) { - int colorTransitionMs = mContext.getResources().getInteger( - R.integer.config_screen_rotation_color_transition); - final float[] rgbTmpFloat = new float[3]; - final int startColor = Color.rgb(mStartLuma, mStartLuma, mStartLuma); - final int endColor = Color.rgb(mEndLuma, mEndLuma, mEndLuma); - final long duration = colorTransitionMs * (long) animationScale; - final Transaction t = mTransactionPool.acquire(); - - final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); - // Animation length is already expected to be scaled. - va.overrideDurationScale(1.0f); - va.setDuration(duration); - va.addUpdateListener(animation -> { - final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); - final float fraction = currentPlayTime / va.getDuration(); - applyColor(startColor, endColor, rgbTmpFloat, fraction, mBackColorSurface, t); - }); - va.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(Animator animation) { - applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface, - t); - mTransactionPool.release(t); - } - - @Override - public void onAnimationEnd(Animator animation) { - applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface, - t); - mTransactionPool.release(t); - } - }); - animExecutor.execute(va::start); + private void buildLumaAnimation(@NonNull ArrayList<Animator> animations, + float startLuma, float endLuma, SurfaceControl surface, float animationScale, + @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { + final long durationMillis = (long) (mContext.getResources().getInteger( + R.integer.config_screen_rotation_color_transition) * animationScale); + final LumaAnimation animation = new LumaAnimation(durationMillis); + // Align the end with the enter animation. + animation.setStartOffset(mRotateEnterAnimation.getDuration() - durationMillis); + final LumaAnimationAdapter adapter = new LumaAnimationAdapter(surface, startLuma, endLuma); + DefaultSurfaceAnimator.buildSurfaceAnimation(animations, animation, finishCallback, + mTransactionPool, mainExecutor, adapter); } public void kill() { @@ -389,21 +389,47 @@ class ScreenRotationAnimation { if (mBackColorSurface != null && mBackColorSurface.isValid()) { t.remove(mBackColorSurface); } + if (mBackEffectSurface != null && mBackEffectSurface.isValid()) { + t.remove(mBackEffectSurface); + } t.apply(); mTransactionPool.release(t); } - private static void applyColor(int startColor, int endColor, float[] rgbFloat, - float fraction, SurfaceControl surface, SurfaceControl.Transaction t) { - final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor, - endColor); - Color middleColor = Color.valueOf(color); - rgbFloat[0] = middleColor.red(); - rgbFloat[1] = middleColor.green(); - rgbFloat[2] = middleColor.blue(); - if (surface.isValid()) { - t.setColor(surface, rgbFloat); + /** A no-op wrapper to provide animation duration. */ + private static class LumaAnimation extends Animation { + LumaAnimation(long durationMillis) { + setDuration(durationMillis); + } + } + + private static class LumaAnimationAdapter extends DefaultSurfaceAnimator.AnimationAdapter { + final float[] mColorArray = new float[3]; + final float mStartLuma; + final float mEndLuma; + final AccelerateInterpolator mInterpolation; + + LumaAnimationAdapter(@NonNull SurfaceControl leash, float startLuma, float endLuma) { + super(leash); + mStartLuma = startLuma; + mEndLuma = endLuma; + // Make the initial progress color lighter if the background is light. That avoids + // darker content when fading into the entering surface. + final float factor = Math.min(3f, (Math.max(0.5f, mStartLuma) - 0.5f) * 10); + Slog.d(TAG, "Luma=" + mStartLuma + " factor=" + factor); + mInterpolation = factor > 0.5f ? new AccelerateInterpolator(factor) : null; + } + + @Override + void applyTransformation(ValueAnimator animator, long currentPlayTime) { + final float fraction = mInterpolation != null + ? mInterpolation.getInterpolation(animator.getAnimatedFraction()) + : animator.getAnimatedFraction(); + final float luma = mStartLuma + fraction * (mEndLuma - mStartLuma); + mColorArray[0] = luma; + mColorArray[1] = luma; + mColorArray[2] = luma; + mTransaction.setColor(mLeash, mColorArray); } - t.apply(); } } 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 d03832d3e85e..d280dcd252b4 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 @@ -87,6 +87,8 @@ import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.FocusTransitionListener; +import com.android.wm.shell.shared.IFocusTransitionListener; import com.android.wm.shell.shared.IHomeTransitionListener; import com.android.wm.shell.shared.IShellTransitions; import com.android.wm.shell.shared.ShellTransitions; @@ -103,6 +105,7 @@ import com.android.wm.shell.transition.tracing.TransitionTracer; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.concurrent.Executor; /** * Plays transition animations. Within this player, each transition has a lifecycle. @@ -224,6 +227,7 @@ public class Transitions implements RemoteCallable<Transitions>, private final ArrayList<TransitionObserver> mObservers = new ArrayList<>(); private HomeTransitionObserver mHomeTransitionObserver; + private FocusTransitionObserver mFocusTransitionObserver; /** List of {@link Runnable} instances to run when the last active transition has finished. */ private final ArrayList<Runnable> mRunWhenIdleQueue = new ArrayList<>(); @@ -309,10 +313,12 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, - @NonNull HomeTransitionObserver observer) { + @NonNull HomeTransitionObserver homeTransitionObserver, + @NonNull FocusTransitionObserver focusTransitionObserver) { this(context, shellInit, new ShellCommandHandler(), shellController, organizer, pool, displayController, mainExecutor, mainHandler, animExecutor, - new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), observer); + new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), + homeTransitionObserver, focusTransitionObserver); } public Transitions(@NonNull Context context, @@ -326,7 +332,8 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer, - @NonNull HomeTransitionObserver observer) { + @NonNull HomeTransitionObserver homeTransitionObserver, + @NonNull FocusTransitionObserver focusTransitionObserver) { mOrganizer = organizer; mContext = context; mMainExecutor = mainExecutor; @@ -345,7 +352,8 @@ public class Transitions implements RemoteCallable<Transitions>, mHandlers.add(mRemoteTransitionHandler); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote"); shellInit.addInitCallback(this::onInit, this); - mHomeTransitionObserver = observer; + mHomeTransitionObserver = homeTransitionObserver; + mFocusTransitionObserver = focusTransitionObserver; if (android.tracing.Flags.perfettoTransitionTracing()) { mTransitionTracer = new PerfettoTransitionTracer(); @@ -384,6 +392,8 @@ public class Transitions implements RemoteCallable<Transitions>, mShellCommandHandler.addCommandCallback("transitions", this, this); mShellCommandHandler.addDumpCallback(this::dump, this); + + registerObserver(mFocusTransitionObserver); } public boolean isRegistered() { @@ -1573,6 +1583,21 @@ public class Transitions implements RemoteCallable<Transitions>, mMainExecutor.execute( () -> mRemoteTransitionHandler.removeFiltered(remoteTransition)); } + + @Override + public void setFocusTransitionListener(FocusTransitionListener listener, + Executor executor) { + mMainExecutor.execute(() -> + mFocusTransitionObserver.setLocalFocusTransitionListener(listener, executor)); + + } + + @Override + public void unsetFocusTransitionListener(FocusTransitionListener listener) { + mMainExecutor.execute(() -> + mFocusTransitionObserver.unsetLocalFocusTransitionListener(listener)); + + } } /** @@ -1634,6 +1659,15 @@ public class Transitions implements RemoteCallable<Transitions>, } @Override + public void setFocusTransitionListener(IFocusTransitionListener listener) { + executeRemoteCallWithTaskPermission(mTransitions, "setFocusTransitionListener", + (transitions) -> { + transitions.mFocusTransitionObserver.setRemoteFocusTransitionListener( + transitions, listener); + }); + } + + @Override public SurfaceControl getHomeTaskOverlayContainer() { SurfaceControl[] result = new SurfaceControl[1]; executeRemoteCallWithTaskPermission(mTransitions, "getHomeTaskOverlayContainer", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt index 226b0fb2e1a1..1be26f080ac8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt @@ -107,4 +107,27 @@ class AdditionalSystemViewContainer( } windowManagerWrapper.updateViewLayout(view, lp) } + + class Factory { + fun create( + windowManagerWrapper: WindowManagerWrapper, + taskId: Int, + x: Int, + y: Int, + width: Int, + height: Int, + flags: Int, + view: View, + ): AdditionalSystemViewContainer = + AdditionalSystemViewContainer( + windowManagerWrapper = windowManagerWrapper, + taskId = taskId, + x = x, + y = y, + width = width, + height = height, + flags = flags, + view = view + ) + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt new file mode 100644 index 000000000000..98413ee96133 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor.education + +import android.annotation.DimenRes +import android.annotation.LayoutRes +import android.content.Context +import android.content.res.Resources +import android.graphics.Point +import android.util.Size +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.View.MeasureSpec.UNSPECIFIED +import android.view.WindowManager +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.SpringForce +import com.android.wm.shell.R +import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.windowdecor.WindowManagerWrapper +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer + +/** + * Controls the lifecycle of an education tooltip, including showing and hiding it. Ensures that + * only one tooltip is displayed at a time. + */ +class DesktopWindowingEducationTooltipController( + private val context: Context, + private val additionalSystemViewContainerFactory: AdditionalSystemViewContainer.Factory, +) { + // TODO: b/369384567 - Set tooltip color scheme to match LT/DT of app theme + private var tooltipView: View? = null + private var animator: PhysicsAnimator<View>? = null + private val springConfig by lazy { + PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY) + } + private var popupWindow: AdditionalSystemViewContainer? = null + + /** + * Shows education tooltip. + * + * @param tooltipViewConfig features of tooltip. + * @param taskId is used in the title of popup window created for the tooltip view. + */ + fun showEducationTooltip(tooltipViewConfig: EducationViewConfig, taskId: Int) { + hideEducationTooltip() + tooltipView = createEducationTooltipView(tooltipViewConfig, taskId) + animator = createAnimator() + animateShowTooltipTransition() + } + + /** Hide the current education view if visible */ + private fun hideEducationTooltip() = animateHideTooltipTransition { cleanUp() } + + /** Create education view by inflating layout provided. */ + private fun createEducationTooltipView( + tooltipViewConfig: EducationViewConfig, + taskId: Int, + ): View { + val tooltipView = + LayoutInflater.from(context) + .inflate( + tooltipViewConfig.tooltipViewLayout, /* root= */ null, /* attachToRoot= */ false) + .apply { + alpha = 0f + scaleX = 0f + scaleY = 0f + + requireViewById<TextView>(R.id.tooltip_text).apply { + text = tooltipViewConfig.tooltipText + } + + setOnTouchListener { _, motionEvent -> + if (motionEvent.action == MotionEvent.ACTION_OUTSIDE) { + hideEducationTooltip() + tooltipViewConfig.onDismissAction() + true + } else { + false + } + } + setOnClickListener { + hideEducationTooltip() + tooltipViewConfig.onEducationClickAction() + } + } + + val tooltipDimens = tooltipDimens(tooltipView = tooltipView, tooltipViewConfig.arrowDirection) + val tooltipViewGlobalCoordinates = + tooltipViewGlobalCoordinates( + tooltipViewGlobalCoordinates = tooltipViewConfig.tooltipViewGlobalCoordinates, + arrowDirection = tooltipViewConfig.arrowDirection, + tooltipDimen = tooltipDimens) + createTooltipPopupWindow( + taskId, tooltipViewGlobalCoordinates, tooltipDimens, tooltipView = tooltipView) + + return tooltipView + } + + /** Create animator for education transitions */ + private fun createAnimator(): PhysicsAnimator<View>? = + tooltipView?.let { + PhysicsAnimator.getInstance(it).apply { setDefaultSpringConfig(springConfig) } + } + + /** Animate show transition for the education view */ + private fun animateShowTooltipTransition() { + animator + ?.spring(DynamicAnimation.ALPHA, 1f) + ?.spring(DynamicAnimation.SCALE_X, 1f) + ?.spring(DynamicAnimation.SCALE_Y, 1f) + ?.start() + } + + /** Animate hide transition for the education view */ + private fun animateHideTooltipTransition(endActions: () -> Unit) { + animator + ?.spring(DynamicAnimation.ALPHA, 0f) + ?.spring(DynamicAnimation.SCALE_X, 0f) + ?.spring(DynamicAnimation.SCALE_Y, 0f) + ?.start() + endActions() + } + + /** Remove education tooltip and clean up all relative properties */ + private fun cleanUp() { + tooltipView = null + animator = null + popupWindow?.releaseView() + popupWindow = null + } + + private fun createTooltipPopupWindow( + taskId: Int, + tooltipViewGlobalCoordinates: Point, + tooltipDimen: Size, + tooltipView: View, + ) { + popupWindow = + additionalSystemViewContainerFactory.create( + windowManagerWrapper = + WindowManagerWrapper(context.getSystemService(WindowManager::class.java)), + taskId = taskId, + x = tooltipViewGlobalCoordinates.x, + y = tooltipViewGlobalCoordinates.y, + width = tooltipDimen.width, + height = tooltipDimen.height, + flags = + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, + view = tooltipView) + } + + private fun tooltipViewGlobalCoordinates( + tooltipViewGlobalCoordinates: Point, + arrowDirection: TooltipArrowDirection, + tooltipDimen: Size, + ): Point { + var tooltipX = tooltipViewGlobalCoordinates.x + var tooltipY = tooltipViewGlobalCoordinates.y + + // Current values of [tooltipX]/[tooltipY] are the coordinates of tip of the arrow. + // Parameter x and y passed to [AdditionalSystemViewContainer] is the top left position of + // the window to be created. Hence we will need to move the coordinates left/up in order + // to position the tooltip correctly. + if (arrowDirection == TooltipArrowDirection.UP) { + // Arrow is placed at horizontal center on top edge of the tooltip. Hence decrement + // half of tooltip width from [tooltipX] to horizontally position the tooltip. + tooltipX -= tooltipDimen.width / 2 + } else { + // Arrow is placed at vertical center on the left edge of the tooltip. Hence decrement + // half of tooltip height from [tooltipY] to vertically position the tooltip. + tooltipY -= tooltipDimen.height / 2 + } + return Point(tooltipX, tooltipY) + } + + private fun tooltipDimens(tooltipView: View, arrowDirection: TooltipArrowDirection): Size { + val tooltipBackground = tooltipView.requireViewById<LinearLayout>(R.id.tooltip_container) + val arrowView = tooltipView.requireViewById<ImageView>(R.id.arrow_icon) + tooltipBackground.measure( + /* widthMeasureSpec= */ UNSPECIFIED, /* heightMeasureSpec= */ UNSPECIFIED) + arrowView.measure(/* widthMeasureSpec= */ UNSPECIFIED, /* heightMeasureSpec= */ UNSPECIFIED) + + var desiredWidth = + tooltipBackground.measuredWidth + + 2 * loadDimensionPixelSize(R.dimen.desktop_windowing_education_tooltip_padding) + var desiredHeight = + tooltipBackground.measuredHeight + + 2 * loadDimensionPixelSize(R.dimen.desktop_windowing_education_tooltip_padding) + if (arrowDirection == TooltipArrowDirection.UP) { + // desiredHeight currently does not account for the height of arrow, hence adding it. + desiredHeight += arrowView.height + } else { + // desiredWidth currently does not account for the width of arrow, hence adding it. + desiredWidth += arrowView.width + } + + return Size(desiredWidth, desiredHeight) + } + + private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int { + if (resourceId == Resources.ID_NULL) return 0 + return context.resources.getDimensionPixelSize(resourceId) + } + + /** + * The configuration for education view features: + * + * @property tooltipViewLayout Layout resource ID of the view to be used for education tooltip. + * @property tooltipViewGlobalCoordinates Global (screen) coordinates of the tip of the tooltip + * arrow. + * @property tooltipText Text to be added to the TextView of tooltip. + * @property arrowDirection Direction of arrow of the tooltip. + * @property onEducationClickAction Lambda to be executed when the tooltip is clicked. + * @property onDismissAction Lambda to be executed when the tooltip is dismissed. + */ + data class EducationViewConfig( + @LayoutRes val tooltipViewLayout: Int, + val tooltipViewGlobalCoordinates: Point, + val tooltipText: String, + val arrowDirection: TooltipArrowDirection, + val onEducationClickAction: () -> Unit, + val onDismissAction: () -> Unit, + ) + + /** Direction of arrow of the tooltip */ + enum class TooltipArrowDirection { + UP, + LEFT, + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml index 40dbbac32c7f..c8df15d81345 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml index 85715db3d952..706c63244890 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml index 6c903a2e8c42..7df1675f541c 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml index 6c903a2e8c42..7df1675f541c 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml index f69a90cc793f..d87c1795cf7b 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml index b76d06565700..99969e71238a 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml index 041978c371ff..19c3e4048d69 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml index bf040d2a95f4..7505860709e9 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="on"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="on"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index 177e47a342f6..c52d9dd24165 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -19,7 +19,7 @@ package com.android.wm.shell.common.split; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; import static com.google.common.truth.Truth.assertThat; @@ -136,7 +136,7 @@ public class SplitLayoutTests extends ShellTestCase { @Test public void testSetDivideRatio() { mSplitLayout.setDividerPosition(200, false /* applyLayoutChange */); - mSplitLayout.setDivideRatio(SNAP_TO_50_50); + mSplitLayout.setDivideRatio(SNAP_TO_2_50_50); assertThat(mSplitLayout.getDividerPosition()).isEqualTo( mSplitLayout.mDividerSnapAlgorithm.getMiddleTarget().position); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index ee545209904f..94e361659090 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -40,6 +40,7 @@ import android.graphics.Point import android.graphics.PointF import android.graphics.Rect import android.os.Binder +import android.os.Bundle import android.os.Handler import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags @@ -2086,16 +2087,13 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) + fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_removesTask() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - assertNotNull(result, "Should handle request").assertRemoveAt(0, task.token) + assertNull(result, "Should not handle request") } @Test @@ -2137,26 +2135,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_backTransition_singleTask_withWallpaper_withBackNav_removesWallpaperAndTask() { - val task = setUpFreeformTask() - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - result.assertRemoveAt(index = 1, task.token) - } - - @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_singleTaskWithToken_noBackNav_removesWallpaper() { + fun handleRequest_backTransition_singleTaskWithToken_removesWallpaper() { val task = setUpFreeformTask() val wallpaperToken = MockToken().token() @@ -2183,23 +2163,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_backTransition_multipleTasks_withWallpaper_withBackNav_removesTask() { - val task1 = setUpFreeformTask() - setUpFreeformTask() - - taskRepository.wallpaperActivityToken = MockToken().token() - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task1.token) - } - - @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun handleRequest_backTransition_multipleTasks_noBackNav_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -2226,29 +2190,11 @@ class DesktopTasksControllerTest : ShellTestCase() { // Should create remove wallpaper transaction assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - result.assertRemoveAt(index = 1, task1.token) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() { - val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) } @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -2261,23 +2207,6 @@ class DesktopTasksControllerTest : ShellTestCase() { // Should create remove wallpaper transaction assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - result.assertRemoveAt(index = 1, task1.token) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() { - val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) } @Test @@ -2937,6 +2866,108 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun newWindow_fromFullscreenOpensInSplit() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask() + val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + runOpenNewWindow(task) + verify(splitScreenController) + .startIntent(any(), anyInt(), any(), any(), + optionsCaptor.capture(), anyOrNull()) + assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun newWindow_fromSplitOpensInSplit() { + setUpLandscapeDisplay() + val task = setUpSplitScreenTask() + val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + runOpenNewWindow(task) + verify(splitScreenController) + .startIntent( + any(), anyInt(), any(), any(), + optionsCaptor.capture(), anyOrNull() + ) + assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun newWindow_fromFreeformAddsNewWindow() { + setUpLandscapeDisplay() + val task = setUpFreeformTask() + val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + runOpenNewWindow(task) + verify(transitions).startTransition(anyInt(), wctCaptor.capture(), anyOrNull()) + assertThat(ActivityOptions.fromBundle(wctCaptor.value.hierarchyOps[0].launchOptions) + .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + } + + private fun runOpenNewWindow(task: RunningTaskInfo) { + markTaskVisible(task) + task.baseActivity = mock(ComponentName::class.java) + task.isFocused = true + runningTasks.add(task) + controller.openNewWindow(task) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromFullscreenOpensInSplit() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask() + val taskToRequest = setUpFreeformTask() + val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + runOpenInstance(task, taskToRequest.taskId) + verify(splitScreenController) + .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) + assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromSplitOpensInSplit() { + setUpLandscapeDisplay() + val task = setUpSplitScreenTask() + val taskToRequest = setUpFreeformTask() + val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + runOpenInstance(task, taskToRequest.taskId) + verify(splitScreenController) + .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) + assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromFreeformAddsNewWindow() { + setUpLandscapeDisplay() + val task = setUpFreeformTask() + val taskToRequest = setUpFreeformTask() + val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + runOpenInstance(task, taskToRequest.taskId) + verify(transitions).startTransition(anyInt(), wctCaptor.capture(), anyOrNull()) + assertThat(ActivityOptions.fromBundle(wctCaptor.value.hierarchyOps[0].launchOptions) + .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + } + + private fun runOpenInstance( + callingTask: RunningTaskInfo, + requestedTaskId: Int + ) { + markTaskVisible(callingTask) + callingTask.baseActivity = mock(ComponentName::class.java) + callingTask.isFocused = true + runningTasks.add(callingTask) + controller.openInstance(callingTask, requestedTaskId) + } + + @Test fun toggleBounds_togglesToStableBounds() { val bounds = Rect(0, 0, 100, 100) val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt new file mode 100644 index 000000000000..c989d1640f80 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.platform.test.annotations.EnableFlags +import android.view.Display.DEFAULT_DISPLAY +import android.view.WindowManager.TRANSIT_TO_BACK +import android.window.IWindowContainerToken +import android.window.TransitionInfo +import android.window.TransitionInfo.Change +import android.window.WindowContainerToken +import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class DesktopTasksTransitionObserverTest { + + @JvmField + @Rule + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this) + .mockStatic(DesktopModeStatus::class.java) + .build()!! + + private val testExecutor = mock<ShellExecutor>() + private val mockShellInit = mock<ShellInit>() + private val transitions = mock<Transitions>() + private val context = mock<Context>() + private val shellTaskOrganizer = mock<ShellTaskOrganizer>() + private val taskRepository = mock<DesktopModeTaskRepository>() + + private lateinit var transitionObserver: DesktopTasksTransitionObserver + private lateinit var shellInit: ShellInit + + @Before + fun setup() { + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + shellInit = spy(ShellInit(testExecutor)) + + transitionObserver = + DesktopTasksTransitionObserver( + context, taskRepository, transitions, shellTaskOrganizer, shellInit + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun backNavigation_taskMinimized() { + val task = createTaskInfo(1) + whenever(taskRepository.getVisibleTaskCount(any())).thenReturn(1) + + transitionObserver.onTransitionReady( + transition = mock(), + info = + createBackNavigationTransition(task), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(taskRepository).minimizeTask(task.displayId, task.taskId) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun backNavigation_nullTaskInfo_taskNotMinimized() { + val task = createTaskInfo(1) + whenever(taskRepository.getVisibleTaskCount(any())).thenReturn(1) + + transitionObserver.onTransitionReady( + transition = mock(), + info = + createBackNavigationTransition(null), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(taskRepository, never()).minimizeTask(task.displayId, task.taskId) + } + + private fun createBackNavigationTransition( + task: RunningTaskInfo? + ): TransitionInfo { + return TransitionInfo(TRANSIT_TO_BACK, 0 /* flags */).apply { + addChange( + Change(mock(), mock()).apply { + mode = TRANSIT_TO_BACK + parent = null + taskInfo = task + flags = flags + } + ) + } + } + + private fun createTaskInfo(id: Int) = + RunningTaskInfo().apply { + taskId = id + displayId = DEFAULT_DISPLAY + configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)) + baseIntent = Intent().apply { + component = ComponentName("package", "component.name") + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt index 0c3f98a324cd..0c100fca2036 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt @@ -30,7 +30,7 @@ import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_FREEFORM import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SINGLE import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SPLIT import com.android.wm.shell.shared.split.SplitBounds -import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50 +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50 import com.google.common.truth.Correspondence import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertThrows @@ -136,7 +136,7 @@ class GroupedRecentTaskInfoTest : ShellTestCase() { assertThat(recentTaskInfoParcel.taskInfo2).isNotNull() assertThat(recentTaskInfoParcel.taskInfo2!!.taskId).isEqualTo(2) assertThat(recentTaskInfoParcel.splitBounds).isNotNull() - assertThat(recentTaskInfoParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_50_50) + assertThat(recentTaskInfoParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_2_50_50) } @Test @@ -185,7 +185,7 @@ class GroupedRecentTaskInfoTest : ShellTestCase() { private fun splitTasksGroupInfo(): GroupedRecentTaskInfo { val task1 = createTaskInfo(id = 1) val task2 = createTaskInfo(id = 2) - val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_50_50) + val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50) return GroupedRecentTaskInfo.forSplitTasks(task1, task2, splitBounds) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index 386253c19c82..753d4cd153ee 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -22,7 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -211,10 +211,10 @@ public class RecentTasksControllerTest extends ShellTestCase { // Verify only one update if the split info is the same SplitBounds bounds1 = new SplitBounds(new Rect(0, 0, 50, 50), - new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_50_50); + new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_2_50_50); mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds1); SplitBounds bounds2 = new SplitBounds(new Rect(0, 0, 50, 50), - new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_50_50); + new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_2_50_50); mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds2); verify(mRecentTasksController, times(1)).notifyRecentTasksChanged(); } @@ -246,9 +246,9 @@ public class RecentTasksControllerTest extends ShellTestCase { // Mark a couple pairs [t2, t4], [t3, t5] SplitBounds pair1Bounds = - new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_50_50); + new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_2_50_50); SplitBounds pair2Bounds = - new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_50_50); + new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_2_50_50); mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds); mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds); @@ -277,9 +277,9 @@ public class RecentTasksControllerTest extends ShellTestCase { // Mark a couple pairs [t2, t4], [t3, t5] SplitBounds pair1Bounds = - new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_50_50); + new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_2_50_50); SplitBounds pair2Bounds = - new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_50_50); + new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_2_50_50); mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds); mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds); @@ -339,7 +339,7 @@ public class RecentTasksControllerTest extends ShellTestCase { setRawList(t1, t2, t3, t4, t5); SplitBounds pair1Bounds = - new SplitBounds(new Rect(), new Rect(), 1, 2, SNAP_TO_50_50); + new SplitBounds(new Rect(), new Rect(), 1, 2, SNAP_TO_2_50_50); mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, pair1Bounds); when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true); @@ -449,7 +449,7 @@ public class RecentTasksControllerTest extends ShellTestCase { // Add a pair SplitBounds pair1Bounds = - new SplitBounds(new Rect(), new Rect(), 2, 3, SNAP_TO_50_50); + new SplitBounds(new Rect(), new Rect(), 2, 3, SNAP_TO_2_50_50); mRecentTasksController.addSplitPair(t2.taskId, t3.taskId, pair1Bounds); reset(mRecentTasksController); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java index 248393cef9ae..be8e6dc3154b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java @@ -1,6 +1,6 @@ package com.android.wm.shell.recents; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -46,21 +46,21 @@ public class SplitBoundsTest extends ShellTestCase { @Test public void testVerticalStacked() { SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect, - TASK_ID_1, TASK_ID_2, SNAP_TO_50_50); + TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50); assertTrue(ssb.appsStackedVertically); } @Test public void testHorizontalStacked() { SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect, - TASK_ID_1, TASK_ID_2, SNAP_TO_50_50); + TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50); assertFalse(ssb.appsStackedVertically); } @Test public void testHorizontalDividerBounds() { SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect, - TASK_ID_1, TASK_ID_2, SNAP_TO_50_50); + TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50); Rect dividerBounds = ssb.visualDividerBounds; assertEquals(0, dividerBounds.left); assertEquals(DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2, dividerBounds.top); @@ -71,7 +71,7 @@ public class SplitBoundsTest extends ShellTestCase { @Test public void testVerticalDividerBounds() { SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect, - TASK_ID_1, TASK_ID_2, SNAP_TO_50_50); + TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50); Rect dividerBounds = ssb.visualDividerBounds; assertEquals(DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2, dividerBounds.left); assertEquals(0, dividerBounds.top); @@ -82,7 +82,7 @@ public class SplitBoundsTest extends ShellTestCase { @Test public void testEqualVerticalTaskPercent() { SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect, - TASK_ID_1, TASK_ID_2, SNAP_TO_50_50); + TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50); float topPercentSpaceTaken = (float) (DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2) / DEVICE_LENGTH; assertEquals(topPercentSpaceTaken, ssb.topTaskPercent, 0.01); } @@ -90,7 +90,7 @@ public class SplitBoundsTest extends ShellTestCase { @Test public void testEqualHorizontalTaskPercent() { SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect, - TASK_ID_1, TASK_ID_2, SNAP_TO_50_50); + TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50); float leftPercentSpaceTaken = (float) (DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2) / DEVICE_WIDTH; assertEquals(leftPercentSpaceTaken, ssb.leftTaskPercent, 0.01); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt index 19c18be44ab1..ac9606350ebd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt @@ -42,19 +42,44 @@ class SplitScreenConstantsTest { SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT, ) assertEquals( - "the value of SNAP_TO_30_70 should be 0", + "the value of SNAP_TO_2_33_66 should be 0", 0, - SplitScreenConstants.SNAP_TO_30_70, + SplitScreenConstants.SNAP_TO_2_33_66, ) assertEquals( - "the value of SNAP_TO_50_50 should be 1", + "the value of SNAP_TO_2_50_50 should be 1", 1, - SplitScreenConstants.SNAP_TO_50_50, + SplitScreenConstants.SNAP_TO_2_50_50, ) assertEquals( - "the value of SNAP_TO_70_30 should be 2", + "the value of SNAP_TO_2_66_33 should be 2", 2, - SplitScreenConstants.SNAP_TO_70_30, + SplitScreenConstants.SNAP_TO_2_66_33, + ) + assertEquals( + "the value of SNAP_TO_2_90_10 should be 3", + 3, + SplitScreenConstants.SNAP_TO_2_90_10, + ) + assertEquals( + "the value of SNAP_TO_2_10_90 should be 4", + 4, + SplitScreenConstants.SNAP_TO_2_10_90, + ) + assertEquals( + "the value of SNAP_TO_3_33_33_33 should be 5", + 5, + SplitScreenConstants.SNAP_TO_3_33_33_33, + ) + assertEquals( + "the value of SNAP_TO_3_45_45_10 should be 6", + 6, + SplitScreenConstants.SNAP_TO_3_45_45_10, + ) + assertEquals( + "the value of SNAP_TO_3_10_45_45 should be 7", + 7, + SplitScreenConstants.SNAP_TO_3_10_45_45, ) } }
\ No newline at end of file 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 a6c16c43c8cb..67eda8bfecd1 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 @@ -74,6 +74,7 @@ import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.DefaultMixedHandler; +import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.Transitions; @@ -429,7 +430,8 @@ public class StageCoordinatorTests extends ShellTestCase { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mTaskOrganizer, mTransactionPool, mock(DisplayController.class), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); shellInit.init(); return t; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java new file mode 100644 index 000000000000..d37b4cf4b4b3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager.RunningTaskInfo; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionInfo.TransitionMode; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.window.flags.Flags; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.shared.IFocusTransitionListener; +import com.android.wm.shell.shared.TransactionPool; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for the focus transition observer. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +@RequiresFlagsEnabled(Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS) +public class FocusTransitionObserverTest extends ShellTestCase { + + static final int SECONDARY_DISPLAY_ID = 1; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private IFocusTransitionListener mListener; + private Transitions mTransition; + private FocusTransitionObserver mFocusTransitionObserver; + + @Before + public void setUp() { + mListener = mock(IFocusTransitionListener.class); + when(mListener.asBinder()).thenReturn(mock(IBinder.class)); + + mFocusTransitionObserver = new FocusTransitionObserver(); + mTransition = + new Transitions(InstrumentationRegistry.getInstrumentation().getTargetContext(), + mock(ShellInit.class), mock(ShellController.class), + mock(ShellTaskOrganizer.class), mock(TransactionPool.class), + mock(DisplayController.class), new TestShellExecutor(), + new Handler(Looper.getMainLooper()), new TestShellExecutor(), + mock(HomeTransitionObserver.class), + mFocusTransitionObserver); + mFocusTransitionObserver.setRemoteFocusTransitionListener(mTransition, mListener); + } + + @Test + public void testTransitionWithMovedToFrontFlagChangesDisplayFocus() throws RemoteException { + final IBinder binder = mock(IBinder.class); + final SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class); + + // Open a task on the default display, which doesn't change display focus because the + // default display already has it. + TransitionInfo info = mock(TransitionInfo.class); + final List<TransitionInfo.Change> changes = new ArrayList<>(); + setupChange(changes, 123 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY, + true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx); + verify(mListener, never()).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID); + clearInvocations(mListener); + + // Open a new task on the secondary display and verify display focus changes to the display. + changes.clear(); + setupChange(changes, 456 /* taskId */, TRANSIT_OPEN, SECONDARY_DISPLAY_ID, + true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx); + verify(mListener, times(1)).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID); + clearInvocations(mListener); + + // Open the first task to front and verify display focus goes back to the default display. + changes.clear(); + setupChange(changes, 123 /* taskId */, TRANSIT_TO_FRONT, DEFAULT_DISPLAY, + true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx); + verify(mListener, times(1)).onFocusedDisplayChanged(DEFAULT_DISPLAY); + clearInvocations(mListener); + + // Open another task on the default display and verify no display focus switch as it's + // already on the default display. + changes.clear(); + setupChange(changes, 789 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY, + true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx); + verify(mListener, never()).onFocusedDisplayChanged(DEFAULT_DISPLAY); + } + + private void setupChange(List<TransitionInfo.Change> changes, int taskId, + @TransitionMode int mode, int displayId, boolean focused) { + TransitionInfo.Change change = mock(TransitionInfo.Change.class); + RunningTaskInfo taskInfo = mock(RunningTaskInfo.class); + taskInfo.taskId = taskId; + taskInfo.isFocused = focused; + when(change.hasFlags(FLAG_MOVED_TO_TOP)).thenReturn(focused); + taskInfo.displayId = displayId; + when(change.getTaskInfo()).thenReturn(taskInfo); + when(change.getMode()).thenReturn(mode); + changes.add(change); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java index 8f49de0a98fb..8dfdfb4dcbcf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java @@ -100,7 +100,8 @@ public class HomeTransitionObserverTest extends ShellTestCase { mHomeTransitionObserver = new HomeTransitionObserver(mContext, mMainExecutor); mTransition = new Transitions(mContext, mock(ShellInit.class), mock(ShellController.class), mOrganizer, mTransactionPool, mDisplayController, mMainExecutor, - mMainHandler, mAnimExecutor, mHomeTransitionObserver); + mMainHandler, mAnimExecutor, mHomeTransitionObserver, + mock(FocusTransitionObserver.class)); mHomeTransitionObserver.setHomeTransitionListener(mTransition, mListener); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index aea14b900647..6cde0569796d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -158,7 +158,8 @@ public class ShellTransitionTests extends ShellTestCase { ShellInit shellInit = mock(ShellInit.class); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); // One from Transitions, one from RootTaskDisplayAreaOrganizer verify(shellInit).addInitCallback(any(), eq(t)); verify(shellInit).addInitCallback(any(), isA(RootTaskDisplayAreaOrganizer.class)); @@ -170,7 +171,8 @@ public class ShellTransitionTests extends ShellTestCase { ShellController shellController = mock(ShellController.class); final Transitions t = new Transitions(mContext, shellInit, shellController, mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); shellInit.init(); verify(shellController, times(1)).addExternalInterface( eq(ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS), any(), any()); @@ -1238,7 +1240,8 @@ public class ShellTransitionTests extends ShellTestCase { final Transitions transitions = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); final RecentsTransitionHandler recentsHandler = new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions, mock(RecentTasksController.class), mock(HomeTransitionObserver.class)); @@ -1780,7 +1783,8 @@ public class ShellTransitionTests extends ShellTestCase { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); shellInit.init(); return t; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt new file mode 100644 index 000000000000..5594981135b1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor.education + +import android.annotation.LayoutRes +import android.content.Context +import android.graphics.Point +import android.testing.AndroidTestingRunner +import android.testing.TestableContext +import android.testing.TestableLooper +import android.testing.TestableResources +import android.view.MotionEvent +import android.view.View +import android.view.WindowManager +import android.widget.TextView +import androidx.test.filters.SmallTest +import com.android.wm.shell.R +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer +import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipArrowDirection +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidTestingRunner::class) +class DesktopWindowingEducationTooltipControllerTest : ShellTestCase() { + @Mock private lateinit var mockWindowManager: WindowManager + @Mock private lateinit var mockViewContainerFactory: AdditionalSystemViewContainer.Factory + private lateinit var testableResources: TestableResources + private lateinit var testableContext: TestableContext + private lateinit var tooltipController: DesktopWindowingEducationTooltipController + private val tooltipViewArgumentCaptor = argumentCaptor<View>() + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testableContext = TestableContext(mContext) + testableResources = + testableContext.orCreateTestableResources.apply { + addOverride(R.dimen.desktop_windowing_education_tooltip_padding, 10) + } + testableContext.addMockSystemService( + Context.LAYOUT_INFLATER_SERVICE, context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)) + testableContext.addMockSystemService(WindowManager::class.java, mockWindowManager) + tooltipController = + DesktopWindowingEducationTooltipController(testableContext, mockViewContainerFactory) + } + + @Test + fun showEducationTooltip_createsTooltipWithCorrectText() { + val tooltipText = "This is a tooltip" + val tooltipViewConfig = createTooltipConfig(tooltipText = tooltipText) + + tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123) + + verify(mockViewContainerFactory, times(1)) + .create( + windowManagerWrapper = any(), + taskId = anyInt(), + x = anyInt(), + y = anyInt(), + width = anyInt(), + height = anyInt(), + flags = anyInt(), + view = tooltipViewArgumentCaptor.capture()) + val tooltipTextView = + tooltipViewArgumentCaptor.lastValue.findViewById<TextView>(R.id.tooltip_text) + assertThat(tooltipTextView.text).isEqualTo(tooltipText) + } + + @Test + fun showEducationTooltip_usesCorrectTaskIdForWindow() { + val tooltipViewConfig = createTooltipConfig() + val taskIdArgumentCaptor = argumentCaptor<Int>() + + tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123) + + verify(mockViewContainerFactory, times(1)) + .create( + windowManagerWrapper = any(), + taskId = taskIdArgumentCaptor.capture(), + x = anyInt(), + y = anyInt(), + width = anyInt(), + height = anyInt(), + flags = anyInt(), + view = anyOrNull()) + assertThat(taskIdArgumentCaptor.lastValue).isEqualTo(123) + } + + @Test + fun showEducationTooltip_tooltipPointsUpwards_horizontallyPositionTooltip() { + val initialTooltipX = 0 + val initialTooltipY = 0 + val tooltipViewConfig = + createTooltipConfig( + arrowDirection = TooltipArrowDirection.UP, + tooltipViewGlobalCoordinates = Point(initialTooltipX, initialTooltipY)) + val tooltipXArgumentCaptor = argumentCaptor<Int>() + val tooltipWidthArgumentCaptor = argumentCaptor<Int>() + + tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123) + + verify(mockViewContainerFactory, times(1)) + .create( + windowManagerWrapper = any(), + taskId = anyInt(), + x = tooltipXArgumentCaptor.capture(), + y = anyInt(), + width = tooltipWidthArgumentCaptor.capture(), + height = anyInt(), + flags = anyInt(), + view = tooltipViewArgumentCaptor.capture()) + val expectedTooltipX = initialTooltipX - tooltipWidthArgumentCaptor.lastValue / 2 + assertThat(tooltipXArgumentCaptor.lastValue).isEqualTo(expectedTooltipX) + } + + @Test + fun showEducationTooltip_tooltipPointsLeft_verticallyPositionTooltip() { + val initialTooltipX = 0 + val initialTooltipY = 0 + val tooltipViewConfig = + createTooltipConfig( + arrowDirection = TooltipArrowDirection.LEFT, + tooltipViewGlobalCoordinates = Point(initialTooltipX, initialTooltipY)) + val tooltipYArgumentCaptor = argumentCaptor<Int>() + val tooltipHeightArgumentCaptor = argumentCaptor<Int>() + + tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123) + + verify(mockViewContainerFactory, times(1)) + .create( + windowManagerWrapper = any(), + taskId = anyInt(), + x = anyInt(), + y = tooltipYArgumentCaptor.capture(), + width = anyInt(), + height = tooltipHeightArgumentCaptor.capture(), + flags = anyInt(), + view = tooltipViewArgumentCaptor.capture()) + val expectedTooltipY = initialTooltipY - tooltipHeightArgumentCaptor.lastValue / 2 + assertThat(tooltipYArgumentCaptor.lastValue).isEqualTo(expectedTooltipY) + } + + @Test + fun showEducationTooltip_touchEventActionOutside_dismissActionPerformed() { + val mockLambda: () -> Unit = mock() + val tooltipViewConfig = createTooltipConfig(onDismissAction = mockLambda) + + tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123) + verify(mockViewContainerFactory, times(1)) + .create( + windowManagerWrapper = any(), + taskId = anyInt(), + x = anyInt(), + y = anyInt(), + width = anyInt(), + height = anyInt(), + flags = anyInt(), + view = tooltipViewArgumentCaptor.capture()) + val motionEvent = + MotionEvent.obtain( + /* downTime= */ 0L, + /* eventTime= */ 0L, + MotionEvent.ACTION_OUTSIDE, + /* x= */ 0f, + /* y= */ 0f, + /* metaState= */ 0) + tooltipViewArgumentCaptor.lastValue.dispatchTouchEvent(motionEvent) + + verify(mockLambda).invoke() + } + + @Test + fun showEducationTooltip_tooltipClicked_onClickActionPerformed() { + val mockLambda: () -> Unit = mock() + val tooltipViewConfig = createTooltipConfig(onEducationClickAction = mockLambda) + + tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123) + verify(mockViewContainerFactory, times(1)) + .create( + windowManagerWrapper = any(), + taskId = anyInt(), + x = anyInt(), + y = anyInt(), + width = anyInt(), + height = anyInt(), + flags = anyInt(), + view = tooltipViewArgumentCaptor.capture()) + tooltipViewArgumentCaptor.lastValue.performClick() + + verify(mockLambda).invoke() + } + + private fun createTooltipConfig( + @LayoutRes tooltipViewLayout: Int = R.layout.desktop_windowing_education_top_arrow_tooltip, + tooltipViewGlobalCoordinates: Point = Point(0, 0), + tooltipText: String = "This is a tooltip", + arrowDirection: TooltipArrowDirection = TooltipArrowDirection.UP, + onEducationClickAction: () -> Unit = {}, + onDismissAction: () -> Unit = {} + ) = + DesktopWindowingEducationTooltipController.EducationViewConfig( + tooltipViewLayout = tooltipViewLayout, + tooltipViewGlobalCoordinates = tooltipViewGlobalCoordinates, + tooltipText = tooltipText, + arrowDirection = arrowDirection, + onEducationClickAction = onEducationClickAction, + onDismissAction = onDismissAction, + ) +} diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index 504e3290b0ae..bb0fc41acd30 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -3,13 +3,20 @@ package com.google.android.appfunctions.sidecar { public final class AppFunctionManager { ctor public AppFunctionManager(android.content.Context); - method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @Deprecated public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); + method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); + field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0 + field public static final int APP_FUNCTION_STATE_DISABLED = 2; // 0x2 + field public static final int APP_FUNCTION_STATE_ENABLED = 1; // 0x1 } public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE"; field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } @@ -39,6 +46,7 @@ package com.google.android.appfunctions.sidecar { field public static final String PROPERTY_RETURN_VALUE = "returnValue"; field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2 field public static final int RESULT_DENIED = 1; // 0x1 + field public static final int RESULT_DISABLED = 6; // 0x6 field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3 field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4 field public static final int RESULT_OK = 0; // 0x0 diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java index b1dd4676a35e..d660926575d1 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java @@ -16,15 +16,22 @@ package com.google.android.appfunctions.sidecar; +import android.Manifest; import android.annotation.CallbackExecutor; +import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.annotation.UserHandleAware; import android.content.Context; +import android.os.CancellationSignal; +import android.os.OutcomeReceiver; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; - /** * Provides app functions related functionalities. * @@ -37,6 +44,39 @@ import java.util.function.Consumer; // TODO(b/357551503): Implement get and set enabled app function APIs. // TODO(b/367329899): Add sidecar library to Android B builds. public final class AppFunctionManager { + /** + * The default state of the app function. Call {@link #setAppFunctionEnabled} with this to reset + * enabled state to the default value. + */ + public static final int APP_FUNCTION_STATE_DEFAULT = 0; + + /** + * The app function is enabled. To enable an app function, call {@link #setAppFunctionEnabled} + * with this value. + */ + public static final int APP_FUNCTION_STATE_ENABLED = 1; + + /** + * The app function is disabled. To disable an app function, call {@link #setAppFunctionEnabled} + * with this value. + */ + public static final int APP_FUNCTION_STATE_DISABLED = 2; + + /** + * The enabled state of the app function. + * + * @hide + */ + @IntDef( + prefix = {"APP_FUNCTION_STATE_"}, + value = { + APP_FUNCTION_STATE_DEFAULT, + APP_FUNCTION_STATE_ENABLED, + APP_FUNCTION_STATE_DISABLED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface EnabledState {} + private final android.app.appfunctions.AppFunctionManager mManager; private final Context mContext; @@ -45,7 +85,7 @@ public final class AppFunctionManager { * * @param context A {@link Context}. * @throws java.lang.IllegalStateException if the underlying {@link - * android.app.appfunctions.AppFunctionManager} is not found. + * android.app.appfunctions.AppFunctionManager} is not found. */ public AppFunctionManager(Context context) { mContext = Objects.requireNonNull(context); @@ -66,6 +106,7 @@ public final class AppFunctionManager { public void executeAppFunction( @NonNull ExecuteAppFunctionRequest sidecarRequest, @NonNull @CallbackExecutor Executor executor, + @NonNull CancellationSignal cancellationSignal, @NonNull Consumer<ExecuteAppFunctionResponse> callback) { Objects.requireNonNull(sidecarRequest); Objects.requireNonNull(executor); @@ -74,9 +115,100 @@ public final class AppFunctionManager { android.app.appfunctions.ExecuteAppFunctionRequest platformRequest = SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest); mManager.executeAppFunction( - platformRequest, executor, (platformResponse) -> { - callback.accept(SidecarConverter.getSidecarExecuteAppFunctionResponse( - platformResponse)); + platformRequest, + executor, + cancellationSignal, + (platformResponse) -> { + callback.accept( + SidecarConverter.getSidecarExecuteAppFunctionResponse( + platformResponse)); }); } + + /** + * Executes the app function. + * + * <p>Proxies request and response to the underlying {@link + * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and + * response in the appropriate type required by the function. + * + * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor, + * CancellationSignal, Consumer)} instead. This method will be removed once usage references + * are updated. + */ + @Deprecated + public void executeAppFunction( + @NonNull ExecuteAppFunctionRequest sidecarRequest, + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + Objects.requireNonNull(sidecarRequest); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + + executeAppFunction( + sidecarRequest, + executor, + new CancellationSignal(), + callback); + } + + /** + * Returns a boolean through a callback, indicating whether the app function is enabled. + * + * <p>* This method can only check app functions owned by the caller, or those where the caller + * has visibility to the owner package and holds either the {@link + * Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link + * Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permission. + * + * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors: + * + * <ul> + * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not + * have access to it. + * </ul> + * + * @param functionIdentifier the identifier of the app function to check (unique within the + * target package) and in most cases, these are automatically generated by the AppFunctions + * SDK + * @param targetPackage the package name of the app function's owner + * @param executor the executor to run the request + * @param callback the callback to receive the function enabled check result + */ + public void isAppFunctionEnabled( + @NonNull String functionIdentifier, + @NonNull String targetPackage, + @NonNull Executor executor, + @NonNull OutcomeReceiver<Boolean, Exception> callback) { + mManager.isAppFunctionEnabled(functionIdentifier, targetPackage, executor, callback); + } + + /** + * Sets the enabled state of the app function owned by the calling package. + * + * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors: + * + * <ul> + * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not + * have access to it. + * </ul> + * + * @param functionIdentifier the identifier of the app function to enable (unique within the + * calling package). In most cases, identifiers are automatically generated by the + * AppFunctions SDK + * @param newEnabledState the new state of the app function + * @param executor the executor to run the callback + * @param callback the callback to receive the result of the function enablement. The call was + * successful if no exception was thrown. + */ + // Constants in @EnabledState should always mirror those in + // android.app.appfunctions.AppFunctionManager. + @SuppressLint("WrongConstant") + @UserHandleAware + public void setAppFunctionEnabled( + @NonNull String functionIdentifier, + @EnabledState int newEnabledState, + @NonNull Executor executor, + @NonNull OutcomeReceiver<Void, Exception> callback) { + mManager.setAppFunctionEnabled(functionIdentifier, newEnabledState, executor, callback); + } } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java index 65959dfdf561..6023c977bd76 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java @@ -25,6 +25,7 @@ import android.app.Service; import android.content.Intent; import android.os.Binder; import android.os.IBinder; +import android.os.CancellationSignal; import java.util.function.Consumer; @@ -69,10 +70,11 @@ public abstract class AppFunctionService extends Service { private final Binder mBinder = android.app.appfunctions.AppFunctionService.createBinder( /* context= */ this, - /* onExecuteFunction= */ (platformRequest, callback) -> { + /* onExecuteFunction= */ (platformRequest, cancellationSignal, callback) -> { AppFunctionService.this.onExecuteFunction( SidecarConverter.getSidecarExecuteAppFunctionRequest( platformRequest), + cancellationSignal, (sidecarResponse) -> { callback.accept( SidecarConverter.getPlatformExecuteAppFunctionResponse( @@ -105,9 +107,42 @@ public abstract class AppFunctionService extends Service { * result using the callback, no matter if the execution was successful or not. * * @param request The function execution request. + * @param cancellationSignal A {@link CancellationSignal} to cancel the request. * @param callback A callback to report back the result. */ @MainThread + public void onExecuteFunction( + @NonNull ExecuteAppFunctionRequest request, + @NonNull CancellationSignal cancellationSignal, + @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + onExecuteFunction(request, callback); + } + + /** + * Called by the system to execute a specific app function. + * + * <p>This method is triggered when the system requests your AppFunctionService to handle a + * particular function you have registered and made available. + * + * <p>To ensure proper routing of function requests, assign a unique identifier to each + * function. This identifier doesn't need to be globally unique, but it must be unique within + * your app. For example, a function to order food could be identified as "orderFood". In most + * cases this identifier should come from the ID automatically generated by the AppFunctions + * SDK. You can determine the specific function to invoke by calling {@link + * ExecuteAppFunctionRequest#getFunctionIdentifier()}. + * + * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker + * thread and dispatch the result with the given callback. You should always report back the + * result using the callback, no matter if the execution was successful or not. + * + * @param request The function execution request. + * @param callback A callback to report back the result. + * + * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal, + * Consumer)} instead. This method will be removed once usage references are updated. + */ + @MainThread + @Deprecated public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull Consumer<ExecuteAppFunctionResponse> callback); diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java index 60c25fae58d1..c7ce95bab7a5 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java @@ -76,6 +76,9 @@ public final class ExecuteAppFunctionResponse { /** The operation was timed out. */ public static final int RESULT_TIMED_OUT = 5; + /** The caller tried to execute a disabled app function. */ + public static final int RESULT_DISABLED = 6; + /** The result code of the app function execution. */ @ResultCode private final int mResultCode; @@ -234,6 +237,7 @@ public final class ExecuteAppFunctionResponse { RESULT_INTERNAL_ERROR, RESULT_INVALID_ARGUMENT, RESULT_TIMED_OUT, + RESULT_DISABLED }) @Retention(RetentionPolicy.SOURCE) public @interface ResultCode {} diff --git a/libs/hwui/hwui/MinikinSkia.cpp b/libs/hwui/hwui/MinikinSkia.cpp index bbb142014ed8..f0bcfe537998 100644 --- a/libs/hwui/hwui/MinikinSkia.cpp +++ b/libs/hwui/hwui/MinikinSkia.cpp @@ -36,7 +36,7 @@ namespace android { 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) + const minikin::VariationSettings& axes) : mTypeface(std::move(typeface)) , mSourceId(sourceId) , mFontData(fontData) @@ -123,12 +123,12 @@ int MinikinFontSkia::GetFontIndex() const { return mTtcIndex; } -const std::vector<minikin::FontVariation>& MinikinFontSkia::GetAxes() const { +const minikin::VariationSettings& MinikinFontSkia::GetAxes() const { return mAxes; } std::shared_ptr<minikin::MinikinFont> MinikinFontSkia::createFontWithVariation( - const std::vector<minikin::FontVariation>& variations) const { + const minikin::VariationSettings& variations) const { SkFontArguments args; std::vector<SkFontArguments::VariationPosition::Coordinate> skVariation; diff --git a/libs/hwui/hwui/MinikinSkia.h b/libs/hwui/hwui/MinikinSkia.h index de9a5c2af0aa..7fe5978bfda3 100644 --- a/libs/hwui/hwui/MinikinSkia.h +++ b/libs/hwui/hwui/MinikinSkia.h @@ -32,7 +32,7 @@ class ANDROID_API MinikinFontSkia : public minikin::MinikinFont { public: 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); + const minikin::VariationSettings& axes); float GetHorizontalAdvance(uint32_t glyph_id, const minikin::MinikinPaint& paint, const minikin::FontFakery& fakery) const override; @@ -59,9 +59,9 @@ public: size_t GetFontSize() const; int GetFontIndex() const; const std::string& getFilePath() const { return mFilePath; } - const std::vector<minikin::FontVariation>& GetAxes() const; + const minikin::VariationSettings& GetAxes() const; std::shared_ptr<minikin::MinikinFont> createFontWithVariation( - const std::vector<minikin::FontVariation>&) const; + const minikin::VariationSettings&) const; int GetSourceId() const override { return mSourceId; } static uint32_t packFontFlags(const SkFont&); @@ -80,7 +80,7 @@ private: const void* mFontData; size_t mFontSize; int mTtcIndex; - std::vector<minikin::FontVariation> mAxes; + minikin::VariationSettings mAxes; std::string mFilePath; }; diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp index a9d1a2aed8cc..2d812d675fdc 100644 --- a/libs/hwui/hwui/Typeface.cpp +++ b/libs/hwui/hwui/Typeface.cpp @@ -92,8 +92,8 @@ Typeface* Typeface::createAbsolute(Typeface* base, int weight, bool italic) { return result; } -Typeface* Typeface::createFromTypefaceWithVariation( - Typeface* src, const std::vector<minikin::FontVariation>& variations) { +Typeface* Typeface::createFromTypefaceWithVariation(Typeface* src, + const minikin::VariationSettings& variations) { const Typeface* resolvedFace = Typeface::resolveDefault(src); Typeface* result = new Typeface(); if (result != nullptr) { @@ -192,9 +192,8 @@ void Typeface::setRobotoTypefaceForTest() { sk_sp<SkTypeface> typeface = fm->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), 0, 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, minikin::VariationSettings()); std::vector<std::shared_ptr<minikin::Font>> fonts; fonts.push_back(minikin::Font::Builder(font).build()); diff --git a/libs/hwui/hwui/Typeface.h b/libs/hwui/hwui/Typeface.h index 565136e53676..2c96c1ad80fe 100644 --- a/libs/hwui/hwui/Typeface.h +++ b/libs/hwui/hwui/Typeface.h @@ -74,8 +74,8 @@ public: static Typeface* createRelative(Typeface* src, Style desiredStyle); static Typeface* createAbsolute(Typeface* base, int weight, bool italic); - static Typeface* createFromTypefaceWithVariation( - Typeface* src, const std::vector<minikin::FontVariation>& variations); + static Typeface* createFromTypefaceWithVariation(Typeface* src, + const minikin::VariationSettings& variations); static Typeface* createFromFamilies( std::vector<std::shared_ptr<minikin::FontFamily>>&& families, int weight, int italic, diff --git a/libs/hwui/jni/FontFamily.cpp b/libs/hwui/jni/FontFamily.cpp index e6d790f56d0f..9922ff393e55 100644 --- a/libs/hwui/jni/FontFamily.cpp +++ b/libs/hwui/jni/FontFamily.cpp @@ -133,9 +133,9 @@ static bool addSkTypeface(NativeFamilyBuilder* builder, sk_sp<SkData>&& data, in builder->axes.clear(); return false; } - std::shared_ptr<minikin::MinikinFont> minikinFont = - std::make_shared<MinikinFontSkia>(std::move(face), fonts::getNewSourceId(), fontPtr, - fontSize, "", ttcIndex, builder->axes); + std::shared_ptr<minikin::MinikinFont> minikinFont = std::make_shared<MinikinFontSkia>( + std::move(face), fonts::getNewSourceId(), fontPtr, fontSize, "", ttcIndex, + minikin::VariationSettings(builder->axes, false)); minikin::Font::Builder fontBuilder(minikinFont); if (weight != RESOLVE_BY_FONT_TABLE) { diff --git a/libs/hwui/jni/PathIterator.cpp b/libs/hwui/jni/PathIterator.cpp index 3884342d8d37..e9de6555935d 100644 --- a/libs/hwui/jni/PathIterator.cpp +++ b/libs/hwui/jni/PathIterator.cpp @@ -20,6 +20,7 @@ #include "GraphicsJNI.h" #include "SkPath.h" #include "SkPoint.h" +#include "graphics_jni_helpers.h" namespace android { @@ -36,6 +37,18 @@ public: return reinterpret_cast<jlong>(new SkPath::RawIter(*path)); } + // A variant of 'next' (below) that is compatible with the host JVM. + static jint nextHost(JNIEnv* env, jclass clazz, jlong iteratorHandle, jfloatArray pointsArray) { + jfloat* points = env->GetFloatArrayElements(pointsArray, 0); +#ifdef __ANDROID__ + jint result = next(iteratorHandle, reinterpret_cast<jlong>(points)); +#else + jint result = next(env, clazz, iteratorHandle, reinterpret_cast<jlong>(points)); +#endif + env->ReleaseFloatArrayElements(pointsArray, points, 0); + return result; + } + // ---------------- @CriticalNative ------------------------- static jint peek(CRITICAL_JNI_PARAMS_COMMA jlong iteratorHandle) { @@ -72,6 +85,7 @@ static const JNINativeMethod methods[] = { {"nPeek", "(J)I", (void*)SkPathIteratorGlue::peek}, {"nNext", "(JJ)I", (void*)SkPathIteratorGlue::next}, + {"nNextHost", "(J[F)I", (void*)SkPathIteratorGlue::nextHost}, }; int register_android_graphics_PathIterator(JNIEnv* env) { diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp index 209b35c5537c..0f458dde8b07 100644 --- a/libs/hwui/jni/Typeface.cpp +++ b/libs/hwui/jni/Typeface.cpp @@ -80,7 +80,8 @@ static jlong Typeface_createFromTypefaceWithVariation(JNIEnv* env, jobject, jlon AxisHelper axis(env, axisObject); variations.push_back(minikin::FontVariation(axis.getTag(), axis.getStyleValue())); } - return toJLong(Typeface::createFromTypefaceWithVariation(toTypeface(familyHandle), variations)); + return toJLong(Typeface::createFromTypefaceWithVariation( + toTypeface(familyHandle), minikin::VariationSettings(variations, false /* sorted */))); } static jlong Typeface_createWeightAlias(JNIEnv* env, jobject, jlong familyHandle, jint weight) { @@ -273,7 +274,7 @@ void MinikinFontSkiaFactory::write(minikin::BufferWriter* writer, const std::string& path = typeface->GetFontPath(); writer->writeString(path); writer->write<int>(typeface->GetFontIndex()); - const std::vector<minikin::FontVariation>& axes = typeface->GetAxes(); + const minikin::VariationSettings& axes = typeface->GetAxes(); writer->writeArray<minikin::FontVariation>(axes.data(), axes.size()); bool hasVerity = getVerity(path); writer->write<int8_t>(static_cast<int8_t>(hasVerity)); diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp index f405abaaf5b4..6a05b6c2626c 100644 --- a/libs/hwui/jni/fonts/Font.cpp +++ b/libs/hwui/jni/fonts/Font.cpp @@ -142,7 +142,7 @@ static jlong Font_Builder_clone(JNIEnv* env, jobject clazz, jlong fontPtr, jlong std::shared_ptr<minikin::MinikinFont> newMinikinFont = std::make_shared<MinikinFontSkia>( std::move(newTypeface), minikinSkia->GetSourceId(), minikinSkia->GetFontData(), minikinSkia->GetFontSize(), minikinSkia->getFilePath(), minikinSkia->GetFontIndex(), - builder->axes); + minikin::VariationSettings(builder->axes, false)); std::shared_ptr<minikin::Font> newFont = minikin::Font::Builder(newMinikinFont) .setWeight(weight) .setSlant(static_cast<minikin::FontStyle::Slant>(italic)) @@ -303,7 +303,7 @@ static jlong Font_getAxisInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr, jint inde var = reader.readArray<minikin::FontVariation>().first[index]; } else { const std::shared_ptr<minikin::MinikinFont>& minikinFont = font->font->baseTypeface(); - var = minikinFont->GetAxes().at(index); + var = minikinFont->GetAxes()[index]; } uint32_t floatBinary = *reinterpret_cast<const uint32_t*>(&var.value); return (static_cast<uint64_t>(var.axisTag) << 32) | static_cast<uint64_t>(floatBinary); |