| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.wm; |
| |
| import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; |
| import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; |
| import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; |
| import static android.view.Display.DEFAULT_DISPLAY; |
| import static android.view.WindowManager.TRANSIT_CHANGE; |
| import static android.view.WindowManager.TRANSIT_CLOSE; |
| import static android.view.WindowManager.TRANSIT_NONE; |
| import static android.view.WindowManager.TRANSIT_OPEN; |
| import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; |
| import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; |
| import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; |
| import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; |
| import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; |
| import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; |
| import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE; |
| import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE; |
| import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE; |
| import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE; |
| import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_NONE; |
| import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN; |
| import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK; |
| import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED; |
| import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR; |
| import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED; |
| import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED; |
| import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED; |
| |
| import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; |
| import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; |
| import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; |
| import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED; |
| import static com.android.server.wm.WindowContainer.POSITION_TOP; |
| import static com.android.server.wm.testing.Assert.assertThrows; |
| |
| import static com.google.common.truth.Truth.assertWithMessage; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.anyBoolean; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.clearInvocations; |
| import static org.mockito.Mockito.doAnswer; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.never; |
| import static org.mockito.Mockito.verify; |
| |
| import android.annotation.NonNull; |
| import android.content.ComponentName; |
| import android.content.Intent; |
| import android.content.pm.ActivityInfo; |
| import android.content.res.Configuration; |
| import android.graphics.Color; |
| import android.graphics.Rect; |
| import android.net.Uri; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.platform.test.annotations.Presubmit; |
| import android.view.RemoteAnimationDefinition; |
| import android.view.SurfaceControl; |
| import android.window.ITaskFragmentOrganizer; |
| import android.window.TaskFragmentAnimationParams; |
| import android.window.TaskFragmentCreationParams; |
| import android.window.TaskFragmentInfo; |
| import android.window.TaskFragmentOperation; |
| import android.window.TaskFragmentOrganizer; |
| import android.window.TaskFragmentOrganizerToken; |
| import android.window.TaskFragmentParentInfo; |
| import android.window.TaskFragmentTransaction; |
| import android.window.WindowContainerToken; |
| import android.window.WindowContainerTransaction; |
| |
| import androidx.test.filters.SmallTest; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.Captor; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| import org.mockito.stubbing.Answer; |
| |
| import java.util.List; |
| |
| /** |
| * Build/Install/Run: |
| * atest WmTests:TaskFragmentOrganizerControllerTest |
| */ |
| @SmallTest |
| @Presubmit |
| @RunWith(WindowTestRunner.class) |
| public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { |
| private static final int TASK_ID = 10; |
| |
| private TaskFragmentOrganizerController mController; |
| private WindowOrganizerController mWindowOrganizerController; |
| private TransitionController mTransitionController; |
| private TaskFragmentOrganizer mOrganizer; |
| private TaskFragmentOrganizerToken mOrganizerToken; |
| private ITaskFragmentOrganizer mIOrganizer; |
| private TaskFragment mTaskFragment; |
| private IBinder mFragmentToken; |
| private WindowContainerTransaction mTransaction; |
| private WindowContainerToken mFragmentWindowToken; |
| private RemoteAnimationDefinition mDefinition; |
| private IBinder mErrorToken; |
| private Rect mTaskFragBounds; |
| |
| @Mock |
| private TaskFragmentInfo mTaskFragmentInfo; |
| @Mock |
| private Task mTask; |
| @Captor |
| private ArgumentCaptor<TaskFragmentTransaction> mTransactionCaptor; |
| |
| @Before |
| public void setup() throws RemoteException { |
| MockitoAnnotations.initMocks(this); |
| mWindowOrganizerController = mAtm.mWindowOrganizerController; |
| mTransitionController = mWindowOrganizerController.mTransitionController; |
| mController = mWindowOrganizerController.mTaskFragmentOrganizerController; |
| mOrganizer = new TaskFragmentOrganizer(Runnable::run); |
| mOrganizerToken = mOrganizer.getOrganizerToken(); |
| mIOrganizer = ITaskFragmentOrganizer.Stub.asInterface(mOrganizerToken.asBinder()); |
| mFragmentToken = new Binder(); |
| mTaskFragment = |
| new TaskFragment(mAtm, mFragmentToken, true /* createdByOrganizer */); |
| mTransaction = new WindowContainerTransaction(); |
| mTransaction.setTaskFragmentOrganizer(mIOrganizer); |
| mFragmentWindowToken = mTaskFragment.mRemoteToken.toWindowContainerToken(); |
| mDefinition = new RemoteAnimationDefinition(); |
| mErrorToken = new Binder(); |
| final Rect displayBounds = mDisplayContent.getBounds(); |
| mTaskFragBounds = new Rect(displayBounds.left, displayBounds.top, displayBounds.centerX(), |
| displayBounds.centerY()); |
| |
| spyOn(mController); |
| spyOn(mOrganizer); |
| spyOn(mTaskFragment); |
| spyOn(mWindowOrganizerController); |
| spyOn(mTransitionController); |
| doReturn(mIOrganizer).when(mTaskFragment).getTaskFragmentOrganizer(); |
| doReturn(mTaskFragmentInfo).when(mTaskFragment).getTaskFragmentInfo(); |
| doReturn(new SurfaceControl()).when(mTaskFragment).getSurfaceControl(); |
| doReturn(mFragmentToken).when(mTaskFragment).getFragmentToken(); |
| doReturn(new Configuration()).when(mTaskFragmentInfo).getConfiguration(); |
| |
| // To prevent it from calling the real server. |
| doNothing().when(mOrganizer).applyTransaction(any(), anyInt(), anyBoolean()); |
| doNothing().when(mOrganizer).onTransactionHandled(any(), any(), anyInt(), anyBoolean()); |
| |
| mController.registerOrganizer(mIOrganizer); |
| } |
| |
| @Test |
| public void testCallTaskFragmentCallbackWithoutRegister_throwsException() { |
| mController.unregisterOrganizer(mIOrganizer); |
| |
| doReturn(mTask).when(mTaskFragment).getTask(); |
| |
| assertThrows(IllegalArgumentException.class, () -> mController |
| .onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment)); |
| |
| assertThrows(IllegalArgumentException.class, () -> mController |
| .onTaskFragmentInfoChanged( |
| mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment)); |
| |
| assertThrows(IllegalArgumentException.class, () -> mController |
| .onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment)); |
| } |
| |
| @Test |
| public void testOnTaskFragmentAppeared() { |
| // No-op when the TaskFragment is not attached. |
| mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| verify(mOrganizer, never()).onTransactionReady(any()); |
| |
| // Send callback when the TaskFragment is attached. |
| setupMockParent(mTaskFragment, mTask); |
| |
| mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| assertTaskFragmentParentInfoChangedTransaction(mTask); |
| assertTaskFragmentAppearedTransaction(); |
| } |
| |
| @Test |
| public void testOnTaskFragmentInfoChanged() { |
| setupMockParent(mTaskFragment, mTask); |
| |
| // No-op if onTaskFragmentAppeared is not called yet. |
| mController.onTaskFragmentInfoChanged(mTaskFragment.getTaskFragmentOrganizer(), |
| mTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| verify(mOrganizer, never()).onTransactionReady(any()); |
| |
| // Call onTaskFragmentAppeared first. |
| mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| verify(mOrganizer).onTransactionReady(any()); |
| |
| // No callback if the info is not changed. |
| clearInvocations(mOrganizer); |
| doReturn(true).when(mTaskFragmentInfo).equalsForTaskFragmentOrganizer(any()); |
| doReturn(new Configuration()).when(mTaskFragmentInfo).getConfiguration(); |
| |
| mController.onTaskFragmentInfoChanged(mTaskFragment.getTaskFragmentOrganizer(), |
| mTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| verify(mOrganizer, never()).onTransactionReady(any()); |
| |
| // Trigger callback if the info is changed. |
| doReturn(false).when(mTaskFragmentInfo).equalsForTaskFragmentOrganizer(any()); |
| |
| mController.onTaskFragmentInfoChanged(mTaskFragment.getTaskFragmentOrganizer(), |
| mTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| assertTaskFragmentInfoChangedTransaction(); |
| } |
| |
| @Test |
| public void testOnTaskFragmentVanished() { |
| mTaskFragment.mTaskFragmentAppearedSent = true; |
| mController.onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| assertTrue(mTaskFragment.mTaskFragmentVanishedSent); |
| assertTaskFragmentVanishedTransaction(); |
| } |
| |
| @Test |
| public void testOnTaskFragmentVanished_clearUpRemaining() { |
| setupMockParent(mTaskFragment, mTask); |
| |
| // Not trigger onTaskFragmentAppeared. |
| mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); |
| mController.onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| assertTrue(mTaskFragment.mTaskFragmentVanishedSent); |
| assertTaskFragmentVanishedTransaction(); |
| |
| // Not trigger onTaskFragmentInfoChanged. |
| // Call onTaskFragmentAppeared before calling onTaskFragmentInfoChanged. |
| mTaskFragment.mTaskFragmentVanishedSent = false; |
| mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); |
| mController.dispatchPendingEvents(); |
| clearInvocations(mOrganizer); |
| doReturn(true).when(mTaskFragmentInfo).equalsForTaskFragmentOrganizer(any()); |
| mController.onTaskFragmentInfoChanged(mTaskFragment.getTaskFragmentOrganizer(), |
| mTaskFragment); |
| mController.onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| assertTaskFragmentVanishedTransaction(); |
| } |
| |
| @Test |
| public void testOnTaskFragmentParentInfoChanged() { |
| setupMockParent(mTaskFragment, mTask); |
| mTask.getTaskFragmentParentInfo().getConfiguration().smallestScreenWidthDp = 10; |
| |
| mController.onTaskFragmentAppeared( |
| mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| assertTaskFragmentParentInfoChangedTransaction(mTask); |
| |
| // No extra parent info changed callback if the info is not changed. |
| clearInvocations(mOrganizer); |
| |
| mController.onTaskFragmentInfoChanged( |
| mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); |
| final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); |
| final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); |
| assertEquals(1, changes.size()); |
| final TaskFragmentTransaction.Change change = changes.get(0); |
| assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, change.getType()); |
| |
| // Trigger callback if the size is changed. |
| clearInvocations(mOrganizer); |
| mTask.getTaskFragmentParentInfo().getConfiguration().smallestScreenWidthDp = 100; |
| mController.onTaskFragmentInfoChanged( |
| mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| assertTaskFragmentParentInfoChangedTransaction(mTask); |
| |
| // Trigger callback if the windowing mode is changed. |
| clearInvocations(mOrganizer); |
| mTask.getTaskFragmentParentInfo().getConfiguration().windowConfiguration |
| .setWindowingMode(WINDOWING_MODE_PINNED); |
| mController.onTaskFragmentInfoChanged( |
| mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| assertTaskFragmentParentInfoChangedTransaction(mTask); |
| } |
| |
| @Test |
| public void testOnTaskFragmentError() { |
| final Throwable exception = new IllegalArgumentException("Test exception"); |
| |
| mController.onTaskFragmentError(mTaskFragment.getTaskFragmentOrganizer(), |
| mErrorToken, null /* taskFragment */, OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS, |
| exception); |
| mController.dispatchPendingEvents(); |
| |
| assertTaskFragmentErrorTransaction(OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS, |
| exception.getClass()); |
| } |
| |
| @Test |
| public void testOnActivityReparentedToTask_activityInOrganizerProcess_useActivityToken() { |
| // Make sure the activity pid/uid is the same as the organizer caller. |
| final int pid = Binder.getCallingPid(); |
| final int uid = Binder.getCallingUid(); |
| final ActivityRecord activity = createActivityRecord(mDisplayContent); |
| // Flush EVENT_APPEARED. |
| mController.dispatchPendingEvents(); |
| final Task task = activity.getTask(); |
| activity.info.applicationInfo.uid = uid; |
| doReturn(pid).when(activity).getPid(); |
| task.effectiveUid = uid; |
| |
| // No need to notify organizer if it is not embedded. |
| mController.onActivityReparentedToTask(activity); |
| mController.dispatchPendingEvents(); |
| |
| verify(mOrganizer, never()).onTransactionReady(any()); |
| |
| // Notify organizer if it was embedded before entered Pip. |
| activity.mLastTaskFragmentOrganizerBeforePip = mIOrganizer; |
| mController.onActivityReparentedToTask(activity); |
| mController.dispatchPendingEvents(); |
| |
| assertActivityReparentedToTaskTransaction(task.mTaskId, activity.intent, activity.token); |
| |
| // Notify organizer if there is any embedded in the Task. |
| final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setOrganizer(mOrganizer) |
| .build(); |
| taskFragment.setTaskFragmentOrganizer(mOrganizer.getOrganizerToken(), uid, |
| DEFAULT_TASK_FRAGMENT_ORGANIZER_PROCESS_NAME); |
| activity.reparent(taskFragment, POSITION_TOP); |
| activity.mLastTaskFragmentOrganizerBeforePip = null; |
| // Flush EVENT_INFO_CHANGED. |
| mController.dispatchPendingEvents(); |
| |
| // Clear invocations now because there will be another transaction for the TaskFragment |
| // change above, triggered by the reparent. We only want to test onActivityReparentedToTask |
| // here. |
| clearInvocations(mOrganizer); |
| mController.onActivityReparentedToTask(activity); |
| mController.dispatchPendingEvents(); |
| |
| assertActivityReparentedToTaskTransaction(task.mTaskId, activity.intent, activity.token); |
| } |
| |
| @Test |
| public void testOnActivityReparentedToTask_activityNotInOrganizerProcess_useTemporaryToken() { |
| final int pid = Binder.getCallingPid(); |
| final int uid = Binder.getCallingUid(); |
| mTaskFragment.setTaskFragmentOrganizer(mOrganizer.getOrganizerToken(), uid, |
| DEFAULT_TASK_FRAGMENT_ORGANIZER_PROCESS_NAME); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| final Task task = createTask(mDisplayContent); |
| task.addChild(mTaskFragment, POSITION_TOP); |
| final ActivityRecord activity = createActivityRecord(task); |
| // Flush EVENT_APPEARED. |
| mController.dispatchPendingEvents(); |
| |
| // Make sure the activity belongs to the same app, but it is in a different pid. |
| activity.info.applicationInfo.uid = uid; |
| doReturn(pid + 1).when(activity).getPid(); |
| task.effectiveUid = uid; |
| |
| // Notify organizer if it was embedded before entered Pip. |
| // Create a temporary token since the activity doesn't belong to the same process. |
| clearInvocations(mOrganizer); |
| activity.mLastTaskFragmentOrganizerBeforePip = mIOrganizer; |
| mController.onActivityReparentedToTask(activity); |
| mController.dispatchPendingEvents(); |
| |
| // Allow organizer to reparent activity in other process using the temporary token. |
| verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); |
| final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); |
| final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); |
| assertFalse(changes.isEmpty()); |
| final TaskFragmentTransaction.Change change = changes.get(0); |
| assertEquals(TYPE_ACTIVITY_REPARENTED_TO_TASK, change.getType()); |
| assertEquals(task.mTaskId, change.getTaskId()); |
| assertIntentsEqualForOrganizer(activity.intent, change.getActivityIntent()); |
| assertNotEquals(activity.token, change.getActivityToken()); |
| mTransaction.reparentActivityToTaskFragment(mFragmentToken, change.getActivityToken()); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| assertEquals(mTaskFragment, activity.getTaskFragment()); |
| // The temporary token can only be used once. |
| assertNull(mController.getReparentActivityFromTemporaryToken(mIOrganizer, |
| change.getActivityToken())); |
| } |
| |
| @Test |
| public void testOnActivityReparentedToTask_untrustedEmbed_notReported() { |
| final int pid = Binder.getCallingPid(); |
| final int uid = Binder.getCallingUid(); |
| mTaskFragment.setTaskFragmentOrganizer(mOrganizer.getOrganizerToken(), uid, |
| DEFAULT_TASK_FRAGMENT_ORGANIZER_PROCESS_NAME); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| final Task task = createTask(mDisplayContent); |
| task.addChild(mTaskFragment, POSITION_TOP); |
| final ActivityRecord activity = createActivityRecord(task); |
| // Flush EVENT_APPEARED. |
| mController.dispatchPendingEvents(); |
| |
| // Make sure the activity is embedded in untrusted mode. |
| activity.info.applicationInfo.uid = uid + 1; |
| doReturn(pid + 1).when(activity).getPid(); |
| task.effectiveUid = uid; |
| doReturn(EMBEDDING_ALLOWED).when(task).isAllowedToEmbedActivity(activity, uid); |
| doReturn(false).when(task).isAllowedToEmbedActivityInTrustedMode(activity, uid); |
| doReturn(true).when(task).isAllowedToEmbedActivityInUntrustedMode(activity); |
| |
| // Notify organizer if it was embedded before entered Pip. |
| // Create a temporary token since the activity doesn't belong to the same process. |
| clearInvocations(mOrganizer); |
| activity.mLastTaskFragmentOrganizerBeforePip = mIOrganizer; |
| mController.onActivityReparentedToTask(activity); |
| mController.dispatchPendingEvents(); |
| |
| // Disallow organizer to reparent activity that is untrusted embedded. |
| verify(mOrganizer, never()).onTransactionReady(mTransactionCaptor.capture()); |
| } |
| |
| @Test |
| public void testOnActivityReparentedToTask_trimReportedIntent() { |
| // Make sure the activity pid/uid is the same as the organizer caller. |
| final int pid = Binder.getCallingPid(); |
| final int uid = Binder.getCallingUid(); |
| final ActivityRecord activity = createActivityRecord(mDisplayContent); |
| final Task task = activity.getTask(); |
| activity.info.applicationInfo.uid = uid; |
| doReturn(pid).when(activity).getPid(); |
| task.effectiveUid = uid; |
| activity.mLastTaskFragmentOrganizerBeforePip = mIOrganizer; |
| |
| // Test the Intent trim in #assertIntentTrimmed |
| activity.intent.setComponent(new ComponentName("TestPackage", "TestClass")) |
| .setPackage("TestPackage") |
| .setAction("TestAction") |
| .setData(mock(Uri.class)) |
| .putExtra("Test", 123) |
| .setFlags(10); |
| |
| mController.onActivityReparentedToTask(activity); |
| mController.dispatchPendingEvents(); |
| |
| assertActivityReparentedToTaskTransaction(task.mTaskId, activity.intent, activity.token); |
| } |
| |
| @Test |
| public void testRegisterRemoteAnimations() { |
| mController.registerRemoteAnimations(mIOrganizer, mDefinition); |
| |
| assertEquals(mDefinition, mController.getRemoteAnimationDefinition(mIOrganizer)); |
| |
| mController.unregisterRemoteAnimations(mIOrganizer); |
| |
| assertNull(mController.getRemoteAnimationDefinition(mIOrganizer)); |
| } |
| |
| @Test |
| public void testApplyTransaction_enforceConfigurationChangeOnOrganizedTaskFragment() { |
| // Throw exception if the transaction is trying to change a window that is not organized by |
| // the organizer. |
| mTransaction.setBounds(mFragmentWindowToken, new Rect(0, 0, 100, 100)); |
| |
| assertApplyTransactionDisallowed(mTransaction); |
| |
| // Allow transaction to change a TaskFragment created by the organizer. |
| mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, |
| "Test:TaskFragmentOrganizer" /* processName */); |
| |
| assertApplyTransactionAllowed(mTransaction); |
| } |
| |
| @Test |
| public void testApplyTransaction_enforceHierarchyChange_deleteTaskFragment() { |
| doReturn(true).when(mTaskFragment).isAttached(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| |
| // Throw exception if the transaction is trying to change a window that is not organized by |
| // the organizer. |
| mTransaction.deleteTaskFragment(mFragmentToken); |
| |
| assertApplyTransactionDisallowed(mTransaction); |
| |
| // Allow transaction to change a TaskFragment created by the organizer. |
| mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, |
| "Test:TaskFragmentOrganizer" /* processName */); |
| clearInvocations(mAtm.mRootWindowContainer); |
| |
| assertApplyTransactionAllowed(mTransaction); |
| verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities(); |
| } |
| |
| @Test |
| public void testApplyTransaction_enforceHierarchyChange_createTaskFragment() { |
| final ActivityRecord ownerActivity = createActivityRecord(mDisplayContent); |
| |
| // Allow organizer to create TaskFragment and start/reparent activity to TaskFragment. |
| createTaskFragmentFromOrganizer(mTransaction, ownerActivity, mFragmentToken); |
| mTransaction.startActivityInTaskFragment( |
| mFragmentToken, null /* callerToken */, new Intent(), null /* activityOptions */); |
| mTransaction.reparentActivityToTaskFragment(mFragmentToken, mock(IBinder.class)); |
| mTransaction.setAdjacentTaskFragments(mFragmentToken, mock(IBinder.class), |
| null /* options */); |
| mTransaction.clearAdjacentTaskFragments(mFragmentToken); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| // Successfully created a TaskFragment. |
| final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment( |
| mFragmentToken); |
| assertNotNull(taskFragment); |
| assertEquals(ownerActivity.getTask(), taskFragment.getTask()); |
| } |
| |
| @Test |
| public void testApplyTransaction_enforceTaskFragmentOrganized_startActivityInTaskFragment() { |
| final Task task = createTask(mDisplayContent); |
| final ActivityRecord ownerActivity = createActivityRecord(task); |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(mFragmentToken) |
| .build(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| mTransaction.startActivityInTaskFragment( |
| mFragmentToken, ownerActivity.token, new Intent(), null /* activityOptions */); |
| mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_OPEN, |
| false /* shouldApplyIndependently */); |
| |
| // Not allowed because TaskFragment is not organized by the caller organizer. |
| assertApplyTransactionDisallowed(mTransaction); |
| |
| mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, |
| "Test:TaskFragmentOrganizer" /* processName */); |
| |
| assertApplyTransactionAllowed(mTransaction); |
| } |
| |
| @Test |
| public void testApplyTransaction_enforceTaskFragmentOrganized_reparentActivityInTaskFragment() { |
| final Task task = createTask(mDisplayContent); |
| final ActivityRecord activity = createActivityRecord(task); |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(mFragmentToken) |
| .build(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token); |
| mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, |
| false /* shouldApplyIndependently */); |
| |
| // Not allowed because TaskFragment is not organized by the caller organizer. |
| assertApplyTransactionDisallowed(mTransaction); |
| |
| mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, |
| "Test:TaskFragmentOrganizer" /* processName */); |
| |
| assertApplyTransactionAllowed(mTransaction); |
| } |
| |
| @Test |
| public void testApplyTransaction_enforceTaskFragmentOrganized_setAdjacentTaskFragments() { |
| final Task task = createTask(mDisplayContent); |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(mFragmentToken) |
| .build(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| final IBinder fragmentToken2 = new Binder(); |
| final TaskFragment taskFragment2 = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(fragmentToken2) |
| .build(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(fragmentToken2, taskFragment2); |
| mTransaction.setAdjacentTaskFragments(mFragmentToken, fragmentToken2, null /* params */); |
| mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, |
| false /* shouldApplyIndependently */); |
| |
| // Not allowed because TaskFragments are not organized by the caller organizer. |
| assertApplyTransactionDisallowed(mTransaction); |
| assertNull(mTaskFragment.getAdjacentTaskFragment()); |
| assertNull(taskFragment2.getAdjacentTaskFragment()); |
| |
| mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, |
| "Test:TaskFragmentOrganizer" /* processName */); |
| |
| // Not allowed because TaskFragment2 is not organized by the caller organizer. |
| assertApplyTransactionDisallowed(mTransaction); |
| assertNull(mTaskFragment.getAdjacentTaskFragment()); |
| assertNull(taskFragment2.getAdjacentTaskFragment()); |
| |
| mTaskFragment.onTaskFragmentOrganizerRemoved(); |
| taskFragment2.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, |
| "Test:TaskFragmentOrganizer" /* processName */); |
| |
| // Not allowed because mTaskFragment is not organized by the caller organizer. |
| assertApplyTransactionDisallowed(mTransaction); |
| assertNull(mTaskFragment.getAdjacentTaskFragment()); |
| assertNull(taskFragment2.getAdjacentTaskFragment()); |
| |
| mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, |
| "Test:TaskFragmentOrganizer" /* processName */); |
| |
| assertApplyTransactionAllowed(mTransaction); |
| assertEquals(taskFragment2, mTaskFragment.getAdjacentTaskFragment()); |
| } |
| |
| @Test |
| public void testApplyTransaction_enforceTaskFragmentOrganized_clearAdjacentTaskFragments() { |
| final Task task = createTask(mDisplayContent); |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(mFragmentToken) |
| .build(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| final IBinder fragmentToken2 = new Binder(); |
| final TaskFragment taskFragment2 = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(fragmentToken2) |
| .build(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(fragmentToken2, taskFragment2); |
| mTaskFragment.setAdjacentTaskFragment(taskFragment2); |
| |
| mTransaction.clearAdjacentTaskFragments(mFragmentToken); |
| mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, |
| false /* shouldApplyIndependently */); |
| |
| // Not allowed because TaskFragment is not organized by the caller organizer. |
| assertApplyTransactionDisallowed(mTransaction); |
| assertEquals(taskFragment2, mTaskFragment.getAdjacentTaskFragment()); |
| |
| mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, |
| "Test:TaskFragmentOrganizer" /* processName */); |
| |
| assertApplyTransactionAllowed(mTransaction); |
| assertNull(mTaskFragment.getAdjacentTaskFragment()); |
| assertNull(taskFragment2.getAdjacentTaskFragment()); |
| } |
| |
| @Test |
| public void testApplyTransaction_enforceTaskFragmentOrganized_requestFocusOnTaskFragment() { |
| final Task task = createTask(mDisplayContent); |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(mFragmentToken) |
| .build(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| mTransaction.requestFocusOnTaskFragment(mFragmentToken); |
| mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, |
| false /* shouldApplyIndependently */); |
| |
| // Not allowed because TaskFragment is not organized by the caller organizer. |
| assertApplyTransactionDisallowed(mTransaction); |
| |
| mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, |
| "Test:TaskFragmentOrganizer" /* processName */); |
| |
| assertApplyTransactionAllowed(mTransaction); |
| } |
| |
| @Test |
| public void testApplyTransaction_enforceTaskFragmentOrganized_addTaskFragmentOperation() { |
| final Task task = createTask(mDisplayContent); |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(mFragmentToken) |
| .build(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( |
| OP_TYPE_SET_ANIMATION_PARAMS) |
| .setAnimationParams(TaskFragmentAnimationParams.DEFAULT) |
| .build(); |
| mTransaction.addTaskFragmentOperation(mFragmentToken, operation); |
| mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, |
| false /* shouldApplyIndependently */); |
| |
| // Not allowed because TaskFragment is not organized by the caller organizer. |
| assertApplyTransactionDisallowed(mTransaction); |
| |
| mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, |
| "Test:TaskFragmentOrganizer" /* processName */); |
| |
| assertApplyTransactionAllowed(mTransaction); |
| } |
| |
| @Test |
| public void testAddTaskFragmentOperation() { |
| final Task task = createTask(mDisplayContent); |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setOrganizer(mOrganizer) |
| .setFragmentToken(mFragmentToken) |
| .build(); |
| assertEquals(TaskFragmentAnimationParams.DEFAULT, mTaskFragment.getAnimationParams()); |
| |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| final TaskFragmentAnimationParams animationParams = |
| new TaskFragmentAnimationParams.Builder() |
| .setAnimationBackgroundColor(Color.GREEN) |
| .build(); |
| final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( |
| OP_TYPE_SET_ANIMATION_PARAMS) |
| .setAnimationParams(animationParams) |
| .build(); |
| mTransaction.addTaskFragmentOperation(mFragmentToken, operation); |
| mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, |
| false /* shouldApplyIndependently */); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| assertEquals(animationParams, mTaskFragment.getAnimationParams()); |
| assertEquals(Color.GREEN, mTaskFragment.getAnimationParams().getAnimationBackgroundColor()); |
| } |
| |
| @Test |
| public void testApplyTransaction_createTaskFragment_failForDifferentUid() { |
| final ActivityRecord activity = createActivityRecord(mDisplayContent); |
| final int uid = Binder.getCallingUid(); |
| final IBinder fragmentToken = new Binder(); |
| final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder( |
| mOrganizerToken, fragmentToken, activity.token).build(); |
| mTransaction.createTaskFragment(params); |
| |
| // Fail to create TaskFragment when the task uid is different from caller. |
| activity.info.applicationInfo.uid = uid; |
| activity.getTask().effectiveUid = uid + 1; |
| assertApplyTransactionAllowed(mTransaction); |
| |
| assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); |
| |
| // Fail to create TaskFragment when the task uid is different from owner activity. |
| activity.info.applicationInfo.uid = uid + 1; |
| activity.getTask().effectiveUid = uid; |
| assertApplyTransactionAllowed(mTransaction); |
| |
| assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); |
| |
| // Successfully created a TaskFragment for same uid. |
| activity.info.applicationInfo.uid = uid; |
| activity.getTask().effectiveUid = uid; |
| assertApplyTransactionAllowed(mTransaction); |
| |
| assertNotNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); |
| } |
| |
| @Test |
| public void testApplyTransaction_createTaskFragment_withPairedPrimaryFragmentToken() { |
| final Task task = createTask(mDisplayContent); |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(mFragmentToken) |
| .createActivityCount(1) |
| .build(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| final ActivityRecord activityOnTop = createActivityRecord(task); |
| final int uid = Binder.getCallingUid(); |
| activityOnTop.info.applicationInfo.uid = uid; |
| activityOnTop.getTask().effectiveUid = uid; |
| final IBinder fragmentToken1 = new Binder(); |
| final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder( |
| mOrganizerToken, fragmentToken1, activityOnTop.token) |
| .setPairedPrimaryFragmentToken(mFragmentToken) |
| .build(); |
| mTransaction.setTaskFragmentOrganizer(mIOrganizer); |
| mTransaction.createTaskFragment(params); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| // Successfully created a TaskFragment. |
| final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment( |
| fragmentToken1); |
| assertNotNull(taskFragment); |
| // The new TaskFragment should be positioned right above the paired TaskFragment. |
| assertEquals(task.mChildren.indexOf(mTaskFragment) + 1, |
| task.mChildren.indexOf(taskFragment)); |
| // The top activity should remain on top. |
| assertEquals(task.mChildren.indexOf(taskFragment) + 1, |
| task.mChildren.indexOf(activityOnTop)); |
| } |
| |
| @Test |
| public void testApplyTransaction_createTaskFragment_overrideBounds() { |
| final Task task = createTask(mDisplayContent); |
| final ActivityRecord activityAtBottom = createActivityRecord(task); |
| final int uid = Binder.getCallingUid(); |
| activityAtBottom.info.applicationInfo.uid = uid; |
| activityAtBottom.getTask().effectiveUid = uid; |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(mFragmentToken) |
| .createActivityCount(1) |
| .build(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| final IBinder fragmentToken1 = new Binder(); |
| final Rect bounds = new Rect(100, 100, 500, 1000); |
| final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder( |
| mOrganizerToken, fragmentToken1, activityAtBottom.token) |
| .setPairedActivityToken(activityAtBottom.token) |
| .setInitialRelativeBounds(bounds) |
| .build(); |
| mTransaction.setTaskFragmentOrganizer(mIOrganizer); |
| mTransaction.createTaskFragment(params); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| // Successfully created a TaskFragment. |
| final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment( |
| fragmentToken1); |
| assertNotNull(taskFragment); |
| // The relative embedded bounds is updated to the initial requested bounds. |
| assertEquals(bounds, taskFragment.getRelativeEmbeddedBounds()); |
| } |
| |
| @Test |
| public void testApplyTransaction_createTaskFragment_withPairedActivityToken() { |
| final Task task = createTask(mDisplayContent); |
| final ActivityRecord activityAtBottom = createActivityRecord(task); |
| final int uid = Binder.getCallingUid(); |
| activityAtBottom.info.applicationInfo.uid = uid; |
| activityAtBottom.getTask().effectiveUid = uid; |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(mFragmentToken) |
| .createActivityCount(1) |
| .build(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| final IBinder fragmentToken1 = new Binder(); |
| final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder( |
| mOrganizerToken, fragmentToken1, activityAtBottom.token) |
| .setPairedActivityToken(activityAtBottom.token) |
| .build(); |
| mTransaction.setTaskFragmentOrganizer(mIOrganizer); |
| mTransaction.createTaskFragment(params); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| // Successfully created a TaskFragment. |
| final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment( |
| fragmentToken1); |
| assertNotNull(taskFragment); |
| // The new TaskFragment should be positioned right above the paired activity. |
| assertEquals(task.mChildren.indexOf(activityAtBottom) + 1, |
| task.mChildren.indexOf(taskFragment)); |
| // The top TaskFragment should remain on top. |
| assertEquals(task.mChildren.indexOf(taskFragment) + 1, |
| task.mChildren.indexOf(mTaskFragment)); |
| } |
| |
| @Test |
| public void testApplyTransaction_reparentActivityToTaskFragment_triggerLifecycleUpdate() { |
| final Task task = createTask(mDisplayContent); |
| final ActivityRecord activity = createActivityRecord(task); |
| // Skip manipulate the SurfaceControl. |
| doNothing().when(activity).setDropInputMode(anyInt()); |
| mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, |
| false /* shouldApplyIndependently */); |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(mFragmentToken) |
| .setOrganizer(mOrganizer) |
| .build(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token); |
| doReturn(EMBEDDING_ALLOWED).when(mTaskFragment).isAllowedToEmbedActivity(activity); |
| clearInvocations(mAtm.mRootWindowContainer); |
| |
| assertApplyTransactionAllowed(mTransaction); |
| |
| verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities(); |
| } |
| |
| @Test |
| public void testApplyTransaction_requestFocusOnTaskFragment() { |
| final Task task = createTask(mDisplayContent); |
| final IBinder token0 = new Binder(); |
| final TaskFragment tf0 = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(token0) |
| .setOrganizer(mOrganizer) |
| .createActivityCount(1) |
| .build(); |
| final IBinder token1 = new Binder(); |
| final TaskFragment tf1 = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(token1) |
| .setOrganizer(mOrganizer) |
| .createActivityCount(1) |
| .build(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(token0, tf0); |
| mWindowOrganizerController.mLaunchTaskFragments.put(token1, tf1); |
| final ActivityRecord activity0 = tf0.getTopMostActivity(); |
| final ActivityRecord activity1 = tf1.getTopMostActivity(); |
| |
| // No effect if the current focus is in a different Task. |
| final ActivityRecord activityInOtherTask = createActivityRecord(mDefaultDisplay); |
| mDisplayContent.setFocusedApp(activityInOtherTask); |
| mTransaction.requestFocusOnTaskFragment(token0); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| assertEquals(activityInOtherTask, mDisplayContent.mFocusedApp); |
| |
| // No effect if there is no resumed activity in the request TaskFragment. |
| activity0.setState(ActivityRecord.State.PAUSED, "test"); |
| activity1.setState(ActivityRecord.State.RESUMED, "test"); |
| mDisplayContent.setFocusedApp(activity1); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| assertEquals(activity1, mDisplayContent.mFocusedApp); |
| |
| // Set focus to the request TaskFragment when the current focus is in the same Task, and it |
| // has a resumed activity. |
| activity0.setState(ActivityRecord.State.RESUMED, "test"); |
| mDisplayContent.setFocusedApp(activity1); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| assertEquals(activity0, mDisplayContent.mFocusedApp); |
| } |
| |
| @Test |
| public void testApplyTransaction_finishActivity() { |
| final ActivityRecord activity = createActivityRecord(mDisplayContent); |
| |
| mTransaction.finishActivity(activity.token); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| assertTrue(activity.finishing); |
| } |
| |
| @Test |
| public void testApplyTransaction_skipTransactionForUnregisterOrganizer() { |
| mController.unregisterOrganizer(mIOrganizer); |
| final ActivityRecord ownerActivity = createActivityRecord(mDisplayContent); |
| final IBinder fragmentToken = new Binder(); |
| |
| // Allow organizer to create TaskFragment and start/reparent activity to TaskFragment. |
| createTaskFragmentFromOrganizer(mTransaction, ownerActivity, fragmentToken); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| // Nothing should happen as the organizer is not registered. |
| assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); |
| |
| mController.registerOrganizer(mIOrganizer); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| // Successfully created when the organizer is registered. |
| assertNotNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); |
| } |
| |
| @Test |
| public void testOnTransactionHandled_skipTransactionForUnregisterOrganizer() { |
| mController.unregisterOrganizer(mIOrganizer); |
| final ActivityRecord ownerActivity = createActivityRecord(mDisplayContent); |
| final IBinder fragmentToken = new Binder(); |
| |
| // Allow organizer to create TaskFragment and start/reparent activity to TaskFragment. |
| createTaskFragmentFromOrganizer(mTransaction, ownerActivity, fragmentToken); |
| mController.onTransactionHandled(new Binder(), mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, |
| false /* shouldApplyIndependently */); |
| |
| // Nothing should happen as the organizer is not registered. |
| assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); |
| } |
| |
| @Test |
| public void testOrganizerRemovedWithPendingEvents() { |
| final TaskFragment tf0 = new TaskFragmentBuilder(mAtm) |
| .setCreateParentTask() |
| .setOrganizer(mOrganizer) |
| .setFragmentToken(mFragmentToken) |
| .build(); |
| final TaskFragment tf1 = new TaskFragmentBuilder(mAtm) |
| .setCreateParentTask() |
| .setOrganizer(mOrganizer) |
| .setFragmentToken(new Binder()) |
| .build(); |
| assertTrue(tf0.isOrganizedTaskFragment()); |
| assertTrue(tf1.isOrganizedTaskFragment()); |
| assertTrue(tf0.isAttached()); |
| assertTrue(tf0.isAttached()); |
| |
| // Mock the behavior that remove TaskFragment can trigger event dispatch. |
| final Answer<Void> removeImmediately = invocation -> { |
| invocation.callRealMethod(); |
| mController.dispatchPendingEvents(); |
| return null; |
| }; |
| doAnswer(removeImmediately).when(tf0).removeImmediately(); |
| doAnswer(removeImmediately).when(tf1).removeImmediately(); |
| |
| // Add pending events. |
| mController.onTaskFragmentAppeared(mIOrganizer, tf0); |
| mController.onTaskFragmentAppeared(mIOrganizer, tf1); |
| |
| // Remove organizer. |
| mController.unregisterOrganizer(mIOrganizer); |
| mController.dispatchPendingEvents(); |
| |
| // Nothing should happen after the organizer is removed. |
| verify(mOrganizer, never()).onTransactionReady(any()); |
| |
| // TaskFragments should be removed. |
| assertFalse(tf0.isOrganizedTaskFragment()); |
| assertFalse(tf1.isOrganizedTaskFragment()); |
| assertFalse(tf0.isAttached()); |
| assertFalse(tf0.isAttached()); |
| } |
| |
| @Test |
| public void testTaskFragmentInPip_startActivityInTaskFragment() { |
| setupTaskFragmentInPip(); |
| final ActivityRecord activity = mTaskFragment.getTopMostActivity(); |
| spyOn(mAtm.getActivityStartController()); |
| spyOn(mWindowOrganizerController); |
| |
| // Not allow to start activity in a TaskFragment that is in a PIP Task. |
| mTransaction.startActivityInTaskFragment( |
| mFragmentToken, activity.token, new Intent(), null /* activityOptions */) |
| .setErrorCallbackToken(mErrorToken); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| verify(mAtm.getActivityStartController(), never()).startActivityInTaskFragment(any(), any(), |
| any(), any(), anyInt(), anyInt(), any()); |
| verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), |
| eq(mErrorToken), eq(mTaskFragment), |
| eq(OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT), |
| any(IllegalArgumentException.class)); |
| } |
| |
| @Test |
| public void testTaskFragmentInPip_reparentActivityToTaskFragment() { |
| setupTaskFragmentInPip(); |
| final ActivityRecord activity = createActivityRecord(mDisplayContent); |
| spyOn(mWindowOrganizerController); |
| |
| // Not allow to reparent activity to a TaskFragment that is in a PIP Task. |
| mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token) |
| .setErrorCallbackToken(mErrorToken); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), |
| eq(mErrorToken), eq(mTaskFragment), |
| eq(OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT), |
| any(IllegalArgumentException.class)); |
| assertNull(activity.getOrganizedTaskFragment()); |
| } |
| |
| @Test |
| public void testTaskFragmentInPip_setAdjacentTaskFragment() { |
| setupTaskFragmentInPip(); |
| spyOn(mWindowOrganizerController); |
| |
| // Not allow to set adjacent on a TaskFragment that is in a PIP Task. |
| mTransaction.setAdjacentTaskFragments(mFragmentToken, new Binder(), null /* options */) |
| .setErrorCallbackToken(mErrorToken); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), |
| eq(mErrorToken), eq(mTaskFragment), eq(OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS), |
| any(IllegalArgumentException.class)); |
| verify(mTaskFragment, never()).setAdjacentTaskFragment(any()); |
| } |
| |
| @Test |
| public void testTaskFragmentInPip_createTaskFragment() { |
| final Task pipTask = createTask(mDisplayContent, WINDOWING_MODE_PINNED, |
| ACTIVITY_TYPE_STANDARD); |
| final ActivityRecord activity = createActivityRecord(pipTask); |
| final IBinder fragmentToken = new Binder(); |
| spyOn(mWindowOrganizerController); |
| |
| // Not allow to create TaskFragment in a PIP Task. |
| createTaskFragmentFromOrganizer(mTransaction, activity, fragmentToken); |
| mTransaction.setErrorCallbackToken(mErrorToken); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), |
| eq(mErrorToken), eq(null), eq(OP_TYPE_CREATE_TASK_FRAGMENT), |
| any(IllegalArgumentException.class)); |
| assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); |
| } |
| |
| @Test |
| public void testTaskFragmentInPip_deleteTaskFragment() { |
| setupTaskFragmentInPip(); |
| spyOn(mWindowOrganizerController); |
| |
| // Not allow to delete a TaskFragment that is in a PIP Task. |
| mTransaction.deleteTaskFragment(mFragmentToken) |
| .setErrorCallbackToken(mErrorToken); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), |
| eq(mErrorToken), eq(mTaskFragment), eq(OP_TYPE_DELETE_TASK_FRAGMENT), |
| any(IllegalArgumentException.class)); |
| assertNotNull(mWindowOrganizerController.getTaskFragment(mFragmentToken)); |
| |
| // Allow organizer to delete empty TaskFragment for cleanup. |
| final Task task = mTaskFragment.getTask(); |
| mTaskFragment.removeChild(mTaskFragment.getTopMostActivity()); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| assertNull(mWindowOrganizerController.getTaskFragment(mFragmentToken)); |
| assertNull(task.getTopChild()); |
| } |
| |
| @Test |
| public void testTaskFragmentInPip_setConfig() { |
| setupTaskFragmentInPip(); |
| spyOn(mWindowOrganizerController); |
| |
| // Set bounds is ignored on a TaskFragment that is in a PIP Task. |
| mTransaction.setBounds(mFragmentWindowToken, new Rect(0, 0, 100, 100)); |
| |
| verify(mTaskFragment, never()).setBounds(any()); |
| } |
| |
| @Test |
| public void testDeferPendingTaskFragmentEventsOfInvisibleTask() { |
| final Task task = createTask(mDisplayContent); |
| final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setOrganizer(mOrganizer) |
| .setFragmentToken(mFragmentToken) |
| .build(); |
| doReturn(false).when(task).shouldBeVisible(any()); |
| |
| // Dispatch the initial event in the Task to update the Task visibility to the organizer. |
| mController.onTaskFragmentAppeared(mIOrganizer, taskFragment); |
| mController.dispatchPendingEvents(); |
| verify(mOrganizer).onTransactionReady(any()); |
| |
| // Verify that events were not sent when the Task is in background. |
| clearInvocations(mOrganizer); |
| final Rect bounds = new Rect(0, 0, 500, 1000); |
| task.setBoundsUnchecked(bounds); |
| mController.onTaskFragmentParentInfoChanged(mIOrganizer, task); |
| mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); |
| mController.dispatchPendingEvents(); |
| verify(mOrganizer, never()).onTransactionReady(any()); |
| |
| // Verify that the events were sent when the Task becomes visible. |
| doReturn(true).when(task).shouldBeVisible(any()); |
| task.lastActiveTime++; |
| mController.dispatchPendingEvents(); |
| verify(mOrganizer).onTransactionReady(any()); |
| } |
| |
| @Test |
| public void testSendAllPendingTaskFragmentEventsWhenAnyTaskIsVisible() { |
| // Invisible Task. |
| final Task invisibleTask = createTask(mDisplayContent); |
| final TaskFragment invisibleTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(invisibleTask) |
| .setOrganizer(mOrganizer) |
| .setFragmentToken(mFragmentToken) |
| .build(); |
| doReturn(false).when(invisibleTask).shouldBeVisible(any()); |
| |
| // Visible Task. |
| final IBinder fragmentToken = new Binder(); |
| final Task visibleTask = createTask(mDisplayContent); |
| final TaskFragment visibleTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(visibleTask) |
| .setOrganizer(mOrganizer) |
| .setFragmentToken(fragmentToken) |
| .build(); |
| doReturn(true).when(invisibleTask).shouldBeVisible(any()); |
| |
| // Sending events |
| invisibleTaskFragment.mTaskFragmentAppearedSent = true; |
| visibleTaskFragment.mTaskFragmentAppearedSent = true; |
| mController.onTaskFragmentInfoChanged(mIOrganizer, invisibleTaskFragment); |
| mController.onTaskFragmentInfoChanged(mIOrganizer, visibleTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| // Verify that both events are sent. |
| verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); |
| final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); |
| final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); |
| |
| // There should be two Task info changed with two TaskFragment info changed. |
| assertEquals(4, changes.size()); |
| // Invisible Task info changed |
| assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(0).getType()); |
| assertEquals(invisibleTask.mTaskId, changes.get(0).getTaskId()); |
| // Invisible TaskFragment info changed |
| assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(1).getType()); |
| assertEquals(invisibleTaskFragment.getFragmentToken(), |
| changes.get(1).getTaskFragmentToken()); |
| // Visible Task info changed |
| assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(2).getType()); |
| assertEquals(visibleTask.mTaskId, changes.get(2).getTaskId()); |
| // Visible TaskFragment info changed |
| assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(3).getType()); |
| assertEquals(visibleTaskFragment.getFragmentToken(), changes.get(3).getTaskFragmentToken()); |
| } |
| |
| @Test |
| public void testCanSendPendingTaskFragmentEventsAfterActivityResumed() { |
| final Task task = createTask(mDisplayContent); |
| final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setOrganizer(mOrganizer) |
| .setFragmentToken(mFragmentToken) |
| .createActivityCount(1) |
| .build(); |
| final ActivityRecord activity = taskFragment.getTopMostActivity(); |
| doReturn(false).when(task).shouldBeVisible(any()); |
| taskFragment.setResumedActivity(null, "test"); |
| |
| // Dispatch the initial event in the Task to update the Task visibility to the organizer. |
| mController.onTaskFragmentAppeared(mIOrganizer, taskFragment); |
| mController.dispatchPendingEvents(); |
| verify(mOrganizer).onTransactionReady(any()); |
| |
| // Verify the info changed event is not sent because the Task is invisible |
| clearInvocations(mOrganizer); |
| final Rect bounds = new Rect(0, 0, 500, 1000); |
| task.setBoundsUnchecked(bounds); |
| mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); |
| mController.dispatchPendingEvents(); |
| verify(mOrganizer, never()).onTransactionReady(any()); |
| |
| // Mock the task becomes visible, and activity resumed. Verify the info changed event is |
| // sent. |
| doReturn(true).when(task).shouldBeVisible(any()); |
| taskFragment.setResumedActivity(activity, "test"); |
| mController.dispatchPendingEvents(); |
| verify(mOrganizer).onTransactionReady(any()); |
| } |
| |
| /** |
| * Tests that a task fragment info changed event is still sent if the task is invisible only |
| * when the info changed event is because of the last activity in a task finishing. |
| */ |
| @Test |
| public void testLastPendingTaskFragmentInfoChangedEventOfInvisibleTaskSent() { |
| // Create a TaskFragment with an activity, all within a parent task |
| final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) |
| .setOrganizer(mOrganizer) |
| .setFragmentToken(mFragmentToken) |
| .setCreateParentTask() |
| .createActivityCount(1) |
| .build(); |
| final Task parentTask = taskFragment.getTask(); |
| final ActivityRecord activity = taskFragment.getTopNonFinishingActivity(); |
| assertTrue(parentTask.shouldBeVisible(null)); |
| |
| // Dispatch pending info changed event from creating the activity |
| taskFragment.mTaskFragmentAppearedSent = true; |
| mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); |
| mController.dispatchPendingEvents(); |
| |
| // Finish the activity and verify that the task is invisible |
| activity.finishing = true; |
| assertFalse(parentTask.shouldBeVisible(null)); |
| |
| // Verify the info changed callback still occurred despite the task being invisible |
| clearInvocations(mOrganizer); |
| mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); |
| mController.dispatchPendingEvents(); |
| verify(mOrganizer).onTransactionReady(any()); |
| } |
| |
| /** |
| * Tests that a task fragment info changed event is sent if the TaskFragment becomes empty |
| * even if the Task is invisible. |
| */ |
| @Test |
| public void testPendingTaskFragmentInfoChangedEvent_emptyTaskFragment() { |
| // Create a TaskFragment with an activity, all within a parent task |
| final Task task = createTask(mDisplayContent); |
| final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setOrganizer(mOrganizer) |
| .setFragmentToken(mFragmentToken) |
| .createActivityCount(1) |
| .build(); |
| final ActivityRecord embeddedActivity = taskFragment.getTopNonFinishingActivity(); |
| // Add another activity in the Task so that it always contains a non-finishing activity. |
| createActivityRecord(task); |
| doReturn(false).when(task).shouldBeVisible(any()); |
| |
| // Dispatch the initial event in the Task to update the Task visibility to the organizer. |
| clearInvocations(mOrganizer); |
| mController.onTaskFragmentAppeared(mIOrganizer, taskFragment); |
| mController.dispatchPendingEvents(); |
| verify(mOrganizer).onTransactionReady(any()); |
| |
| // Verify the info changed event is not sent because the Task is invisible |
| clearInvocations(mOrganizer); |
| final Rect bounds = new Rect(0, 0, 500, 1000); |
| task.setBoundsUnchecked(bounds); |
| mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); |
| mController.dispatchPendingEvents(); |
| verify(mOrganizer, never()).onTransactionReady(any()); |
| |
| // Finish the embedded activity, and verify the info changed event is sent because the |
| // TaskFragment is becoming empty. |
| embeddedActivity.finishing = true; |
| mController.dispatchPendingEvents(); |
| verify(mOrganizer).onTransactionReady(any()); |
| } |
| |
| /** |
| * When an embedded {@link TaskFragment} is removed, we should clean up the reference in the |
| * {@link WindowOrganizerController}. |
| */ |
| @Test |
| public void testTaskFragmentRemoved_cleanUpEmbeddedTaskFragment() { |
| final ActivityRecord ownerActivity = createActivityRecord(mDisplayContent); |
| final IBinder fragmentToken = new Binder(); |
| createTaskFragmentFromOrganizer(mTransaction, ownerActivity, fragmentToken); |
| assertApplyTransactionAllowed(mTransaction); |
| final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment(fragmentToken); |
| |
| assertNotNull(taskFragment); |
| |
| taskFragment.removeImmediately(); |
| |
| assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); |
| } |
| |
| /** |
| * For config change to untrusted embedded TaskFragment, we only allow bounds change within |
| * its parent bounds. |
| */ |
| @Test |
| public void testUntrustedEmbedding_configChange() { |
| mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, |
| "Test:TaskFragmentOrganizer" /* processName */); |
| doReturn(false).when(mTaskFragment).isAllowedToBeEmbeddedInTrustedMode(); |
| final Task task = createTask(mDisplayContent); |
| final Rect taskBounds = new Rect(task.getBounds()); |
| final Rect taskAppBounds = new Rect(task.getWindowConfiguration().getAppBounds()); |
| final int taskScreenWidthDp = task.getConfiguration().screenWidthDp; |
| final int taskScreenHeightDp = task.getConfiguration().screenHeightDp; |
| final int taskSmallestScreenWidthDp = task.getConfiguration().smallestScreenWidthDp; |
| task.addChild(mTaskFragment, POSITION_TOP); |
| |
| // Throw exception if the transaction is trying to change bounds of an untrusted outside of |
| // its parent's. |
| |
| // setBounds |
| final Rect tfBounds = new Rect(taskBounds); |
| tfBounds.right++; |
| mTransaction.setBounds(mFragmentWindowToken, tfBounds); |
| assertApplyTransactionDisallowed(mTransaction); |
| |
| mTransaction.setBounds(mFragmentWindowToken, taskBounds); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| // setAppBounds |
| final Rect tfAppBounds = new Rect(taskAppBounds); |
| tfAppBounds.right++; |
| mTransaction.setAppBounds(mFragmentWindowToken, tfAppBounds); |
| assertApplyTransactionDisallowed(mTransaction); |
| |
| mTransaction.setAppBounds(mFragmentWindowToken, taskAppBounds); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| // setScreenSizeDp |
| mTransaction.setScreenSizeDp(mFragmentWindowToken, taskScreenWidthDp + 1, |
| taskScreenHeightDp + 1); |
| assertApplyTransactionDisallowed(mTransaction); |
| |
| mTransaction.setScreenSizeDp(mFragmentWindowToken, taskScreenWidthDp, taskScreenHeightDp); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| // setSmallestScreenWidthDp |
| mTransaction.setSmallestScreenWidthDp(mFragmentWindowToken, taskSmallestScreenWidthDp + 1); |
| assertApplyTransactionDisallowed(mTransaction); |
| |
| mTransaction.setSmallestScreenWidthDp(mFragmentWindowToken, taskSmallestScreenWidthDp); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| // Any of the change mask is not allowed. |
| mTransaction.setFocusable(mFragmentWindowToken, false); |
| assertApplyTransactionDisallowed(mTransaction); |
| } |
| |
| // TODO(b/232871351): add test for minimum dimension violation in startActivityInTaskFragment |
| @Test |
| public void testMinDimensionViolation_ReparentActivityToTaskFragment() { |
| final Task task = createTask(mDisplayContent); |
| final ActivityRecord activity = createActivityRecord(task); |
| // Make minWidth/minHeight exceeds the TaskFragment bounds. |
| activity.info.windowLayout = new ActivityInfo.WindowLayout( |
| 0, 0, 0, 0, 0, mTaskFragBounds.width() + 10, mTaskFragBounds.height() + 10); |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(mFragmentToken) |
| .setOrganizer(mOrganizer) |
| .setBounds(mTaskFragBounds) |
| .build(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| |
| // Reparent activity to mTaskFragment, which is smaller than activity's |
| // minimum dimensions. |
| mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token) |
| .setErrorCallbackToken(mErrorToken); |
| assertApplyTransactionAllowed(mTransaction); |
| // The pending event will be dispatched on the handler (from requestTraversal). |
| waitHandlerIdle(mWm.mAnimationHandler); |
| |
| assertTaskFragmentErrorTransaction(OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT, |
| SecurityException.class); |
| } |
| |
| @Test |
| public void testMinDimensionViolation_SetBounds() { |
| final Task task = createTask(mDisplayContent); |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .createActivityCount(1) |
| .setFragmentToken(mFragmentToken) |
| .setOrganizer(mOrganizer) |
| .setBounds(new Rect(0, 0, mTaskFragBounds.right * 2, mTaskFragBounds.bottom * 2)) |
| .build(); |
| final ActivityRecord activity = mTaskFragment.getTopMostActivity(); |
| // Make minWidth/minHeight exceeds the TaskFragment bounds. |
| activity.info.windowLayout = new ActivityInfo.WindowLayout( |
| 0, 0, 0, 0, 0, mTaskFragBounds.width() + 10, mTaskFragBounds.height() + 10); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| clearInvocations(mAtm.mRootWindowContainer); |
| |
| // Shrink the TaskFragment to mTaskFragBounds to make its bounds smaller than activity's |
| // minimum dimensions. |
| mTransaction.setBounds(mTaskFragment.mRemoteToken.toWindowContainerToken(), mTaskFragBounds) |
| .setErrorCallbackToken(mErrorToken); |
| assertApplyTransactionAllowed(mTransaction); |
| |
| assertWithMessage("setBounds must not be performed.") |
| .that(mTaskFragment.getBounds()).isEqualTo(task.getBounds()); |
| } |
| |
| @Test |
| public void testOnTransactionReady_invokeOnTransactionHandled() { |
| final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); |
| mOrganizer.onTransactionReady(transaction); |
| |
| // Organizer should always trigger #onTransactionHandled when receives #onTransactionReady |
| verify(mOrganizer).onTransactionHandled(eq(transaction.getTransactionToken()), any(), |
| anyInt(), anyBoolean()); |
| verify(mOrganizer, never()).applyTransaction(any(), anyInt(), anyBoolean()); |
| } |
| |
| @Test |
| public void testDispatchTransaction_deferTransitionReady() { |
| setupMockParent(mTaskFragment, mTask); |
| final ArgumentCaptor<IBinder> tokenCaptor = ArgumentCaptor.forClass(IBinder.class); |
| final ArgumentCaptor<WindowContainerTransaction> wctCaptor = |
| ArgumentCaptor.forClass(WindowContainerTransaction.class); |
| doReturn(true).when(mTransitionController).isCollecting(); |
| doReturn(10).when(mTransitionController).getCollectingTransitionId(); |
| |
| mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); |
| mController.dispatchPendingEvents(); |
| |
| // Defer transition when send TaskFragment transaction during transition collection. |
| verify(mTransitionController).deferTransitionReady(); |
| verify(mOrganizer).onTransactionHandled(tokenCaptor.capture(), wctCaptor.capture(), |
| anyInt(), anyBoolean()); |
| |
| final IBinder transactionToken = tokenCaptor.getValue(); |
| final WindowContainerTransaction wct = wctCaptor.getValue(); |
| wct.setTaskFragmentOrganizer(mIOrganizer); |
| mController.onTransactionHandled(transactionToken, wct, TASK_FRAGMENT_TRANSIT_CHANGE, |
| false /* shouldApplyIndependently */); |
| |
| verify(mTransitionController).continueTransitionReady(); |
| } |
| |
| @Test |
| public void testWindowOrganizerApplyTransaction_throwException() { |
| // Not allow to use #applyTransaction(WindowContainerTransaction). |
| assertThrows(RuntimeException.class, () -> mOrganizer.applyTransaction(mTransaction)); |
| |
| // Allow to use the overload method. |
| mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, |
| false /* shouldApplyIndependently */); |
| } |
| |
| @Test |
| public void testTaskFragmentTransitionType() { |
| // 1-1 relationship with WindowManager.TransitionType |
| assertEquals(TRANSIT_NONE, TASK_FRAGMENT_TRANSIT_NONE); |
| assertEquals(TRANSIT_OPEN, TASK_FRAGMENT_TRANSIT_OPEN); |
| assertEquals(TRANSIT_CLOSE, TASK_FRAGMENT_TRANSIT_CLOSE); |
| assertEquals(TRANSIT_CHANGE, TASK_FRAGMENT_TRANSIT_CHANGE); |
| } |
| |
| @Test |
| public void testApplyTransaction_setRelativeBounds() { |
| final Task task = createTask(mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, |
| ACTIVITY_TYPE_STANDARD); |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(mFragmentToken) |
| .build(); |
| final WindowContainerToken token = mTaskFragment.mRemoteToken.toWindowContainerToken(); |
| final Rect relBounds = new Rect(0, 0, 100, 1000); |
| mTransaction.setRelativeBounds(token, relBounds); |
| |
| // Not allowed because TaskFragment is not organized by the caller organizer. |
| assertApplyTransactionDisallowed(mTransaction); |
| |
| mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, |
| "Test:TaskFragmentOrganizer" /* processName */); |
| |
| assertApplyTransactionAllowed(mTransaction); |
| assertEquals(relBounds, mTaskFragment.getRelativeEmbeddedBounds()); |
| assertEquals(relBounds, mTaskFragment.getBounds()); |
| |
| // TaskFragment bounds should be updated to the same relative bounds. |
| final Rect taskBounds = new Rect(200, 200, 700, 1500); |
| task.setBoundsUnchecked(taskBounds); |
| assertEquals(taskBounds, task.getBounds()); |
| assertEquals(relBounds, mTaskFragment.getRelativeEmbeddedBounds()); |
| assertEquals(mTaskFragment.translateRelativeBoundsToAbsoluteBounds(relBounds, taskBounds), |
| mTaskFragment.getBounds()); |
| assertEquals(new Rect(200, 200, 300, 1200), mTaskFragment.getBounds()); |
| |
| // Set TaskFragment to fill Task |
| mTransaction.setRelativeBounds(token, new Rect()); |
| |
| assertApplyTransactionAllowed(mTransaction); |
| assertEquals(new Rect(), mTaskFragment.getRelativeEmbeddedBounds()); |
| assertEquals(taskBounds, mTaskFragment.getBounds()); |
| } |
| |
| @Test |
| public void testUntrustedEmbedding_setRelativeBounds_adjustToTaskBounds() { |
| final Task task = createTask(mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, |
| ACTIVITY_TYPE_STANDARD); |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setParentTask(task) |
| .setFragmentToken(mFragmentToken) |
| .build(); |
| final WindowContainerToken token = mTaskFragment.mRemoteToken.toWindowContainerToken(); |
| mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, |
| "Test:TaskFragmentOrganizer" /* processName */); |
| // Untrusted embedded |
| doReturn(false).when(mTaskFragment).isAllowedToBeEmbeddedInTrustedMode(); |
| |
| final Rect taskBounds = new Rect(0, 0, 500, 1000); |
| task.setBoundsUnchecked(taskBounds); |
| |
| final Rect taskFragmentBounds = new Rect(250, 0, 750, 1000); |
| mTransaction.setRelativeBounds(token, taskFragmentBounds); |
| |
| // Allow operation in case the Task is also resizing. |
| // Adjust the relBounds to the intersection. |
| assertApplyTransactionAllowed(mTransaction); |
| // Relative bounds is correctly set. |
| assertEquals(taskFragmentBounds, mTaskFragment.getRelativeEmbeddedBounds()); |
| // The actual window bounds is adjusted to fit the Task bounds. |
| assertEquals(new Rect(250, 0, 500, 1000), mTaskFragment.getBounds()); |
| |
| // Adjust to the full requested bounds when the Task is resized. |
| taskBounds.set(0, 0, 750, 1000); |
| task.setBoundsUnchecked(taskBounds); |
| assertEquals(taskFragmentBounds, mTaskFragment.getBounds()); |
| } |
| |
| /** |
| * Creates a {@link TaskFragment} with the {@link WindowContainerTransaction}. Calls |
| * {@link WindowOrganizerController#applyTransaction(WindowContainerTransaction)} to apply the |
| * transaction, |
| */ |
| private void createTaskFragmentFromOrganizer(WindowContainerTransaction wct, |
| ActivityRecord ownerActivity, IBinder fragmentToken) { |
| final int uid = Binder.getCallingUid(); |
| ownerActivity.info.applicationInfo.uid = uid; |
| ownerActivity.getTask().effectiveUid = uid; |
| final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder( |
| mOrganizerToken, fragmentToken, ownerActivity.token).build(); |
| wct.setTaskFragmentOrganizer(mIOrganizer); |
| |
| // Allow organizer to create TaskFragment and start/reparent activity to TaskFragment. |
| wct.createTaskFragment(params); |
| } |
| |
| /** Asserts that applying the given transaction will throw a {@link SecurityException}. */ |
| private void assertApplyTransactionDisallowed(WindowContainerTransaction t) { |
| assertThrows(SecurityException.class, () -> |
| mController.applyTransaction(t, TASK_FRAGMENT_TRANSIT_CHANGE, |
| false /* shouldApplyIndependently */)); |
| } |
| |
| /** Asserts that applying the given transaction will not throw any exception. */ |
| private void assertApplyTransactionAllowed(WindowContainerTransaction t) { |
| mController.applyTransaction(t, TASK_FRAGMENT_TRANSIT_CHANGE, |
| false /* shouldApplyIndependently */); |
| } |
| |
| /** Asserts that there will be a transaction for TaskFragment appeared. */ |
| private void assertTaskFragmentAppearedTransaction() { |
| verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); |
| final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); |
| final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); |
| assertFalse(changes.isEmpty()); |
| |
| // Use remove to verify multiple transaction changes. |
| final TaskFragmentTransaction.Change change = changes.remove(0); |
| assertEquals(TYPE_TASK_FRAGMENT_APPEARED, change.getType()); |
| assertEquals(mTaskFragmentInfo, change.getTaskFragmentInfo()); |
| assertEquals(mFragmentToken, change.getTaskFragmentToken()); |
| } |
| |
| /** Asserts that there will be a transaction for TaskFragment info changed. */ |
| private void assertTaskFragmentInfoChangedTransaction() { |
| verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); |
| final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); |
| final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); |
| assertFalse(changes.isEmpty()); |
| |
| // Use remove to verify multiple transaction changes. |
| final TaskFragmentTransaction.Change change = changes.remove(0); |
| assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, change.getType()); |
| assertEquals(mTaskFragmentInfo, change.getTaskFragmentInfo()); |
| assertEquals(mFragmentToken, change.getTaskFragmentToken()); |
| } |
| |
| /** Asserts that there will be a transaction for TaskFragment vanished. */ |
| private void assertTaskFragmentVanishedTransaction() { |
| verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); |
| final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); |
| final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); |
| assertFalse(changes.isEmpty()); |
| |
| // Use remove to verify multiple transaction changes. |
| final TaskFragmentTransaction.Change change = changes.remove(0); |
| assertEquals(TYPE_TASK_FRAGMENT_VANISHED, change.getType()); |
| assertEquals(mTaskFragmentInfo, change.getTaskFragmentInfo()); |
| assertEquals(mFragmentToken, change.getTaskFragmentToken()); |
| } |
| |
| /** Asserts that there will be a transaction for TaskFragment vanished. */ |
| private void assertTaskFragmentParentInfoChangedTransaction(@NonNull Task task) { |
| verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); |
| final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); |
| final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); |
| assertFalse(changes.isEmpty()); |
| |
| // Use remove to verify multiple transaction changes. |
| final TaskFragmentTransaction.Change change = changes.remove(0); |
| assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, change.getType()); |
| assertEquals(task.mTaskId, change.getTaskId()); |
| assertEquals(task.getTaskFragmentParentInfo(), change.getTaskFragmentParentInfo()); |
| } |
| |
| /** Asserts that there will be a transaction for TaskFragment error. */ |
| private void assertTaskFragmentErrorTransaction(@TaskFragmentOperation.OperationType int opType, |
| @NonNull Class<?> exceptionClass) { |
| verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); |
| final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); |
| final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); |
| assertFalse(changes.isEmpty()); |
| |
| // Use remove to verify multiple transaction changes. |
| final TaskFragmentTransaction.Change change = changes.remove(0); |
| assertEquals(TYPE_TASK_FRAGMENT_ERROR, change.getType()); |
| assertEquals(mErrorToken, change.getErrorCallbackToken()); |
| final Bundle errorBundle = change.getErrorBundle(); |
| assertEquals(opType, errorBundle.getInt(KEY_ERROR_CALLBACK_OP_TYPE)); |
| assertEquals(exceptionClass, errorBundle.getSerializable( |
| KEY_ERROR_CALLBACK_THROWABLE, Throwable.class).getClass()); |
| } |
| |
| /** Asserts that there will be a transaction for activity reparented to Task. */ |
| private void assertActivityReparentedToTaskTransaction(int taskId, @NonNull Intent intent, |
| @NonNull IBinder activityToken) { |
| verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); |
| final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); |
| final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); |
| assertFalse(changes.isEmpty()); |
| |
| // Use remove to verify multiple transaction changes. |
| final TaskFragmentTransaction.Change change = changes.remove(0); |
| assertEquals(TYPE_ACTIVITY_REPARENTED_TO_TASK, change.getType()); |
| assertEquals(taskId, change.getTaskId()); |
| assertIntentsEqualForOrganizer(intent, change.getActivityIntent()); |
| assertIntentTrimmed(change.getActivityIntent()); |
| assertEquals(activityToken, change.getActivityToken()); |
| } |
| |
| /** Setups an embedded TaskFragment in a PIP Task. */ |
| private void setupTaskFragmentInPip() { |
| mTaskFragment = new TaskFragmentBuilder(mAtm) |
| .setCreateParentTask() |
| .setFragmentToken(mFragmentToken) |
| .setOrganizer(mOrganizer) |
| .createActivityCount(1) |
| .build(); |
| mFragmentWindowToken = mTaskFragment.mRemoteToken.toWindowContainerToken(); |
| mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); |
| mTaskFragment.getTask().setWindowingMode(WINDOWING_MODE_PINNED); |
| } |
| |
| /** Setups the mock Task as the parent of the given TaskFragment. */ |
| private static void setupMockParent(TaskFragment taskFragment, Task mockParent) { |
| doReturn(mockParent).when(taskFragment).getTask(); |
| doReturn(new TaskFragmentParentInfo(new Configuration(), DEFAULT_DISPLAY, true)) |
| .when(mockParent).getTaskFragmentParentInfo(); |
| |
| // Task needs to be visible |
| mockParent.lastActiveTime = 100; |
| doReturn(true).when(mockParent).shouldBeVisible(any()); |
| } |
| |
| private static void assertIntentsEqualForOrganizer(@NonNull Intent expected, |
| @NonNull Intent actual) { |
| assertEquals(expected.getComponent(), actual.getComponent()); |
| assertEquals(expected.getPackage(), actual.getPackage()); |
| assertEquals(expected.getAction(), actual.getAction()); |
| } |
| |
| private static void assertIntentTrimmed(@NonNull Intent intent) { |
| assertNull(intent.getData()); |
| assertNull(intent.getExtras()); |
| assertEquals(0, intent.getFlags()); |
| } |
| } |