diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 2ac41a7c1c1d2..2d21a87bca101 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -518,7 +518,16 @@ class TaskFragment extends WindowContainer { || isAllowedToEmbedActivityInTrustedMode(a); } + /** + * Checks if the organized task fragment is allowed to embed activity in untrusted mode. + */ boolean isAllowedToEmbedActivityInUntrustedMode(@NonNull ActivityRecord a) { + final WindowContainer parent = getParent(); + if (parent == null || !parent.getBounds().contains(getBounds())) { + // Without full trust between the host and the embedded activity, we don't allow + // TaskFragment to have bounds outside of the parent bounds. + return false; + } return (a.info.flags & FLAG_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING) == FLAG_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING; } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index d862012942353..05a8719073185 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -19,8 +19,6 @@ package com.android.server.wm; import static android.Manifest.permission.START_TASKS_FROM_RECENTS; import static android.app.ActivityManager.isStartResultSuccessful; import static android.view.Display.DEFAULT_DISPLAY; -import static android.window.WindowContainerTransaction.Change.CHANGE_BOUNDS_TRANSACTION; -import static android.window.WindowContainerTransaction.Change.CHANGE_BOUNDS_TRANSACTION_RECT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT; @@ -680,7 +678,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub break; } } - effects |= deleteTaskFragment(taskFragment, errorCallbackToken); + effects |= deleteTaskFragment(taskFragment, organizer, errorCallbackToken); break; } case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: { @@ -698,8 +696,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub .startActivityInTaskFragment(tf, activityIntent, activityOptions, hop.getCallingActivity(), caller.mUid, caller.mPid); if (!isStartResultSuccessful(result)) { - sendTaskFragmentOperationFailure(tf.getTaskFragmentOrganizer(), - errorCallbackToken, + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, convertStartFailureToThrowable(result, activityIntent)); } else { effects |= TRANSACT_EFFECTS_LIFECYCLE; @@ -709,13 +706,20 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: { final IBinder fragmentToken = hop.getNewParent(); final ActivityRecord activity = ActivityRecord.forTokenLocked(hop.getContainer()); - if (!mLaunchTaskFragments.containsKey(fragmentToken) || activity == null) { + final TaskFragment parent = mLaunchTaskFragments.get(fragmentToken); + if (parent == null || activity == null) { final Throwable exception = new IllegalArgumentException( "Not allowed to operate with invalid fragment token or activity."); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception); break; } - activity.reparent(mLaunchTaskFragments.get(fragmentToken), POSITION_TOP); + if (!parent.isAllowedToEmbedActivity(activity)) { + final Throwable exception = new SecurityException( + "The task fragment is not trusted to embed the given activity."); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception); + break; + } + activity.reparent(parent, POSITION_TOP); effects |= TRANSACT_EFFECTS_LIFECYCLE; break; } @@ -877,12 +881,14 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final WindowContainer newParent = hop.getNewParent() != null ? WindowContainer.fromBinder(hop.getNewParent()) : null; - if (oldParent == null || !oldParent.isAttached()) { + if (oldParent == null || oldParent.asTaskFragment() == null + || !oldParent.isAttached()) { Slog.e(TAG, "Attempt to operate on unknown or detached container: " + oldParent); break; } - reparentTaskFragment(oldParent, newParent, errorCallbackToken); + reparentTaskFragment(oldParent.asTaskFragment(), newParent, organizer, + errorCallbackToken); effects |= TRANSACT_EFFECTS_LIFECYCLE; break; } @@ -1238,7 +1244,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub mService.enforceTaskPermission(func); } - private void enforceTaskPermission(String func, WindowContainerTransaction t) { + private void enforceTaskPermission(String func, @Nullable WindowContainerTransaction t) { if (t == null || t.getTaskFragmentOrganizer() == null) { enforceTaskPermission(func); return; @@ -1263,14 +1269,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub while (entries.hasNext()) { final Map.Entry entry = entries.next(); // Only allow to apply changes to TaskFragment that is created by this organizer. - WindowContainer wc = WindowContainer.fromBinder(entry.getKey()); + final WindowContainer wc = WindowContainer.fromBinder(entry.getKey()); enforceTaskFragmentOrganized(func, wc, organizer); enforceTaskFragmentConfigChangeAllowed(func, wc, entry.getValue(), organizer); } - // TODO(b/197364677): Enforce safety of hierarchy operations in untrusted mode. E.g. one - // could first change a trusted TF, and then start/reparent untrusted activity there. - // Hierarchy changes final List hops = t.getHierarchyOps(); for (int i = hops.size() - 1; i >= 0; i--) { @@ -1344,8 +1347,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub * Makes sure that SurfaceControl transactions and the ability to set bounds outside of the * parent bounds are not allowed for embedding without full trust between the host and the * target. - * TODO(b/197364677): Allow SC transactions when the client-driven animations are protected from - * tapjacking. */ private void enforceTaskFragmentConfigChangeAllowed(String func, @Nullable WindowContainer wc, WindowContainerTransaction.Change change, ITaskFragmentOrganizer organizer) { @@ -1353,35 +1354,48 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub Slog.e(TAG, "Attempt to operate on task fragment that no longer exists"); return; } - // Check if TaskFragment is embedded in fully trusted mode - if (wc.asTaskFragment().isAllowedToBeEmbeddedInTrustedMode()) { - // Fully trusted, no need to check further - return; - } - if (change == null) { return; } final int changeMask = change.getChangeMask(); - if ((changeMask & (CHANGE_BOUNDS_TRANSACTION | CHANGE_BOUNDS_TRANSACTION_RECT)) != 0) { + if (changeMask != 0) { + // None of the change should be requested from a TaskFragment organizer. String msg = "Permission Denial: " + func + " from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() - + " trying to apply SurfaceControl changes to TaskFragment in non-trusted " - + "embedding mode, TaskFragmentOrganizer=" + organizer; + + " trying to apply changes of " + changeMask + " to TaskFragment" + + " TaskFragmentOrganizer=" + organizer; + Slog.w(TAG, msg); + throw new SecurityException(msg); + } + // Check if TaskFragment is embedded in fully trusted mode. + if (wc.asTaskFragment().isAllowedToBeEmbeddedInTrustedMode()) { + // Fully trusted, no need to check further + return; + } + final WindowContainer wcParent = wc.getParent(); + if (wcParent == null) { + Slog.e(TAG, "Attempt to apply config change on task fragment that has no parent"); + return; + } + final Configuration requestedConfig = change.getConfiguration(); + final Configuration parentConfig = wcParent.getConfiguration(); + if (parentConfig.screenWidthDp < requestedConfig.screenWidthDp + || parentConfig.screenHeightDp < requestedConfig.screenHeightDp + || parentConfig.smallestScreenWidthDp < requestedConfig.smallestScreenWidthDp) { + String msg = "Permission Denial: " + func + " from pid=" + + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() + + " trying to apply screen width/height greater than parent's for non-trusted" + + " host, TaskFragmentOrganizer=" + organizer; Slog.w(TAG, msg); throw new SecurityException(msg); } if (change.getWindowSetMask() == 0) { - // Nothing else to check. + // No bounds change. return; } - WindowConfiguration requestedWindowConfig = change.getConfiguration().windowConfiguration; - WindowContainer wcParent = wc.getParent(); - if (wcParent == null) { - Slog.e(TAG, "Attempt to set bounds on task fragment that has no parent"); - return; - } - if (!wcParent.getBounds().contains(requestedWindowConfig.getBounds())) { + final WindowConfiguration requestedWindowConfig = requestedConfig.windowConfiguration; + final WindowConfiguration parentWindowConfig = parentConfig.windowConfiguration; + if (!parentWindowConfig.getBounds().contains(requestedWindowConfig.getBounds())) { String msg = "Permission Denial: " + func + " from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() + " trying to apply bounds outside of parent for non-trusted host," @@ -1389,6 +1403,17 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub Slog.w(TAG, msg); throw new SecurityException(msg); } + if (requestedWindowConfig.getAppBounds() != null + && parentWindowConfig.getAppBounds() != null + && !parentWindowConfig.getAppBounds().contains( + requestedWindowConfig.getAppBounds())) { + String msg = "Permission Denial: " + func + " from pid=" + + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() + + " trying to apply app bounds outside of parent for non-trusted host," + + " TaskFragmentOrganizer=" + organizer; + Slog.w(TAG, msg); + throw new SecurityException(msg); + } } void createTaskFragment(@NonNull TaskFragmentCreationParams creationParams, @@ -1414,7 +1439,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (ownerActivity.getTask().effectiveUid != ownerActivity.getUid() || ownerActivity.getTask().effectiveUid != caller.mUid) { final Throwable exception = - new IllegalArgumentException("Not allowed to operate with the ownerToken while " + new SecurityException("Not allowed to operate with the ownerToken while " + "the root activity of the target task belong to the different app"); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception); return; @@ -1431,33 +1456,46 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub mLaunchTaskFragments.put(creationParams.getFragmentToken(), taskFragment); } - void reparentTaskFragment(@NonNull WindowContainer oldParent, - @Nullable WindowContainer newParent, @Nullable IBinder errorCallbackToken) { - WindowContainer parent = newParent; - if (parent == null && oldParent.asTaskFragment() != null) { - parent = oldParent.asTaskFragment().getTask(); + void reparentTaskFragment(@NonNull TaskFragment oldParent, @Nullable WindowContainer newParent, + @Nullable ITaskFragmentOrganizer organizer, @Nullable IBinder errorCallbackToken) { + final TaskFragment newParentTF; + if (newParent == null) { + // Use the old parent's parent if the caller doesn't specify the new parent. + newParentTF = oldParent.getTask(); + } else { + newParentTF = newParent.asTaskFragment(); } - if (parent == null) { + if (newParentTF == null) { final Throwable exception = new IllegalArgumentException("Not allowed to operate with invalid container"); - sendTaskFragmentOperationFailure(oldParent.asTaskFragment().getTaskFragmentOrganizer(), - errorCallbackToken, exception); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception); return; } + if (newParentTF.getTaskFragmentOrganizer() != null) { + // We are reparenting activities to a new embedded TaskFragment, this operation is only + // allowed if the new parent is trusted by all reparent activities. + final boolean isEmbeddingDisallowed = oldParent.forAllActivities(activity -> + !newParentTF.isAllowedToEmbedActivity(activity)); + if (isEmbeddingDisallowed) { + final Throwable exception = new SecurityException( + "The new parent is not trusted to embed the activities."); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception); + return; + } + } while (oldParent.hasChild()) { - oldParent.getChildAt(0).reparent(parent, POSITION_TOP); + oldParent.getChildAt(0).reparent(newParentTF, POSITION_TOP); } } private int deleteTaskFragment(@NonNull TaskFragment taskFragment, - @Nullable IBinder errorCallbackToken) { + @Nullable ITaskFragmentOrganizer organizer, @Nullable IBinder errorCallbackToken) { final int index = mLaunchTaskFragments.indexOfValue(taskFragment); if (index < 0) { final Throwable exception = new IllegalArgumentException("Not allowed to operate with invalid " + "taskFragment"); - sendTaskFragmentOperationFailure(taskFragment.getTaskFragmentOrganizer(), - errorCallbackToken, exception); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception); return 0; } mLaunchTaskFragments.removeAt(index); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index c6158662d110c..c948b87cd4510 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -18,6 +18,7 @@ package com.android.server.wm; 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.WindowContainer.POSITION_TOP; import static com.android.server.wm.testing.Assert.assertThrows; import static org.junit.Assert.assertEquals; @@ -249,19 +250,13 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // the organizer. mTransaction.setBounds(mFragmentWindowToken, new Rect(0, 0, 100, 100)); - assertThrows(SecurityException.class, () -> { - try { - mAtm.getWindowOrganizerController().applyTransaction(mTransaction); - } catch (RemoteException e) { - fail(); - } - }); + assertApplyTransactionDisallowed(mTransaction); // Allow transaction to change a TaskFragment created by the organizer. mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, "Test:TaskFragmentOrganizer" /* processName */); - mAtm.getWindowOrganizerController().applyTransaction(mTransaction); + assertApplyTransactionAllowed(mTransaction); } @Test @@ -272,19 +267,13 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // the organizer. mTransaction.reorder(mFragmentWindowToken, true /* onTop */); - assertThrows(SecurityException.class, () -> { - try { - mAtm.getWindowOrganizerController().applyTransaction(mTransaction); - } catch (RemoteException e) { - fail(); - } - }); + assertApplyTransactionDisallowed(mTransaction); // Allow transaction to change a TaskFragment created by the organizer. mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, "Test:TaskFragmentOrganizer" /* processName */); - mAtm.getWindowOrganizerController().applyTransaction(mTransaction); + assertApplyTransactionAllowed(mTransaction); } @Test @@ -298,27 +287,21 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // the organizer. mTransaction.deleteTaskFragment(mFragmentWindowToken); - assertThrows(SecurityException.class, () -> { - try { - mAtm.getWindowOrganizerController().applyTransaction(mTransaction); - } catch (RemoteException e) { - fail(); - } - }); + assertApplyTransactionDisallowed(mTransaction); // Allow transaction to change a TaskFragment created by the organizer. mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, "Test:TaskFragmentOrganizer" /* processName */); clearInvocations(mAtm.mRootWindowContainer); - mAtm.getWindowOrganizerController().applyTransaction(mTransaction); + assertApplyTransactionAllowed(mTransaction); // No lifecycle update when the TaskFragment is not recorded. verify(mAtm.mRootWindowContainer, never()).resumeFocusedTasksTopActivities(); mAtm.mWindowOrganizerController.mLaunchTaskFragments .put(mFragmentToken, mTaskFragment); - mAtm.getWindowOrganizerController().applyTransaction(mTransaction); + assertApplyTransactionAllowed(mTransaction); verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities(); } @@ -335,13 +318,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // the organizer. mTransaction.setAdjacentRoots(mFragmentWindowToken, token2, false /* moveTogether */); - assertThrows(SecurityException.class, () -> { - try { - mAtm.getWindowOrganizerController().applyTransaction(mTransaction); - } catch (RemoteException e) { - fail(); - } - }); + assertApplyTransactionDisallowed(mTransaction); // Allow transaction to change a TaskFragment created by the organizer. mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, @@ -350,7 +327,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { "Test:TaskFragmentOrganizer" /* processName */); clearInvocations(mAtm.mRootWindowContainer); - mAtm.getWindowOrganizerController().applyTransaction(mTransaction); + assertApplyTransactionAllowed(mTransaction); verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities(); } @@ -423,20 +400,14 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // the organizer. mTransaction.reparentChildren(mFragmentWindowToken, null /* newParent */); - assertThrows(SecurityException.class, () -> { - try { - mAtm.getWindowOrganizerController().applyTransaction(mTransaction); - } catch (RemoteException e) { - fail(); - } - }); + assertApplyTransactionDisallowed(mTransaction); // Allow transaction to change a TaskFragment created by the organizer. mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, "Test:TaskFragmentOrganizer" /* processName */); clearInvocations(mAtm.mRootWindowContainer); - mAtm.getWindowOrganizerController().applyTransaction(mTransaction); + assertApplyTransactionAllowed(mTransaction); verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities(); } @@ -454,6 +425,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { mAtm.mWindowOrganizerController.mLaunchTaskFragments .put(mFragmentToken, mTaskFragment); mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token); + doReturn(true).when(mTaskFragment).isAllowedToEmbedActivity(activity); clearInvocations(mAtm.mRootWindowContainer); mAtm.getWindowOrganizerController().applyTransaction(mTransaction); @@ -540,6 +512,66 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { assertNull(mAtm.mWindowOrganizerController.getTaskFragment(fragmentToken)); } + /** + * For config change to untrusted embedded TaskFragment, we only allow bounds change within + * its parent bounds. + */ + @Test + public void testUntrustedEmbedding_configChange() throws RemoteException { + mController.registerOrganizer(mIOrganizer); + mOrganizer.applyTransaction(mTransaction); + 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); + } + /** * Creates a {@link TaskFragment} with the {@link WindowContainerTransaction}. Calls * {@link WindowOrganizerController#applyTransaction} to apply the transaction, @@ -556,4 +588,24 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // 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, () -> { + try { + mAtm.getWindowOrganizerController().applyTransaction(t); + } catch (RemoteException e) { + fail(); + } + }); + } + + /** Asserts that applying the given transaction will not throw any exception. */ + private void assertApplyTransactionAllowed(WindowContainerTransaction t) { + try { + mAtm.getWindowOrganizerController().applyTransaction(t); + } catch (RemoteException e) { + fail(); + } + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index bd1f9d536c28e..6e0d8549defc7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -1264,6 +1264,7 @@ class WindowTestsBase extends SystemServiceTestsBase { mOrganizer.getOrganizerToken(), DEFAULT_TASK_FRAGMENT_ORGANIZER_UID, DEFAULT_TASK_FRAGMENT_ORGANIZER_PROCESS_NAME); } + spyOn(taskFragment); return taskFragment; } }