1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.window.extensions.embedding; 18 19 import static android.app.ActivityTaskManager.INVALID_TASK_ID; 20 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; 21 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; 22 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; 23 import static android.app.WindowConfiguration.inMultiWindowMode; 24 25 import android.app.Activity; 26 import android.app.ActivityClient; 27 import android.app.WindowConfiguration; 28 import android.app.WindowConfiguration.WindowingMode; 29 import android.content.res.Configuration; 30 import android.graphics.Rect; 31 import android.os.IBinder; 32 import android.util.ArraySet; 33 import android.util.Log; 34 import android.window.TaskFragmentInfo; 35 import android.window.TaskFragmentParentInfo; 36 import android.window.WindowContainerTransaction; 37 38 import androidx.annotation.NonNull; 39 import androidx.annotation.Nullable; 40 41 import java.util.ArrayList; 42 import java.util.List; 43 import java.util.Set; 44 45 /** Represents TaskFragments and split pairs below a Task. */ 46 class TaskContainer { 47 private static final String TAG = TaskContainer.class.getSimpleName(); 48 49 /** The unique task id. */ 50 private final int mTaskId; 51 52 /** Active TaskFragments in this Task. */ 53 @NonNull 54 private final List<TaskFragmentContainer> mContainers = new ArrayList<>(); 55 56 /** Active split pairs in this Task. */ 57 @NonNull 58 private final List<SplitContainer> mSplitContainers = new ArrayList<>(); 59 60 /** Active pin split pair in this Task. */ 61 @Nullable 62 private SplitPinContainer mSplitPinContainer; 63 64 @NonNull 65 private final Configuration mConfiguration; 66 67 private int mDisplayId; 68 69 private boolean mIsVisible; 70 71 /** 72 * TaskFragments that the organizer has requested to be closed. They should be removed when 73 * the organizer receives 74 * {@link SplitController#onTaskFragmentVanished(WindowContainerTransaction, TaskFragmentInfo)} 75 * event for them. 76 */ 77 final Set<IBinder> mFinishedContainer = new ArraySet<>(); 78 79 /** 80 * The {@link TaskContainer} constructor 81 * 82 * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with 83 * {@code activityInTask}. 84 * @param activityInTask The {@link Activity} in the Task with {@code taskId}. It is used to 85 * initialize the {@link TaskContainer} properties. 86 * 87 */ TaskContainer(int taskId, @NonNull Activity activityInTask)88 TaskContainer(int taskId, @NonNull Activity activityInTask) { 89 if (taskId == INVALID_TASK_ID) { 90 throw new IllegalArgumentException("Invalid Task id"); 91 } 92 mTaskId = taskId; 93 final TaskProperties taskProperties = TaskProperties 94 .getTaskPropertiesFromActivity(activityInTask); 95 mConfiguration = taskProperties.getConfiguration(); 96 mDisplayId = taskProperties.getDisplayId(); 97 // Note that it is always called when there's a new Activity is started, which implies 98 // the host task is visible. 99 mIsVisible = true; 100 } 101 getTaskId()102 int getTaskId() { 103 return mTaskId; 104 } 105 getDisplayId()106 int getDisplayId() { 107 return mDisplayId; 108 } 109 isVisible()110 boolean isVisible() { 111 return mIsVisible; 112 } 113 114 @NonNull getTaskProperties()115 TaskProperties getTaskProperties() { 116 return new TaskProperties(mDisplayId, mConfiguration); 117 } 118 updateTaskFragmentParentInfo(@onNull TaskFragmentParentInfo info)119 void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) { 120 mConfiguration.setTo(info.getConfiguration()); 121 mDisplayId = info.getDisplayId(); 122 mIsVisible = info.isVisible(); 123 } 124 125 /** 126 * Returns the windowing mode for the TaskFragments below this Task, which should be split with 127 * other TaskFragments. 128 * 129 * @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when 130 * the pair of TaskFragments are stacked due to the limited space. 131 */ 132 @WindowingMode getWindowingModeForSplitTaskFragment(@ullable Rect taskFragmentBounds)133 int getWindowingModeForSplitTaskFragment(@Nullable Rect taskFragmentBounds) { 134 // Only set to multi-windowing mode if the pair are showing side-by-side. Otherwise, it 135 // will be set to UNDEFINED which will then inherit the Task windowing mode. 136 if (taskFragmentBounds == null || taskFragmentBounds.isEmpty() || isInPictureInPicture()) { 137 return WINDOWING_MODE_UNDEFINED; 138 } 139 // We use WINDOWING_MODE_MULTI_WINDOW when the Task is fullscreen. 140 // However, when the Task is in other multi windowing mode, such as Freeform, we need to 141 // have the activity windowing mode to match the Task, otherwise things like 142 // DecorCaptionView won't work correctly. As a result, have the TaskFragment to be in the 143 // Task windowing mode if the Task is in multi window. 144 // TODO we won't need this anymore after we migrate Freeform caption to WM Shell. 145 return isInMultiWindow() ? getWindowingMode() : WINDOWING_MODE_MULTI_WINDOW; 146 } 147 isInPictureInPicture()148 boolean isInPictureInPicture() { 149 return getWindowingMode() == WINDOWING_MODE_PINNED; 150 } 151 isInMultiWindow()152 boolean isInMultiWindow() { 153 return WindowConfiguration.inMultiWindowMode(getWindowingMode()); 154 } 155 156 @WindowingMode getWindowingMode()157 private int getWindowingMode() { 158 return mConfiguration.windowConfiguration.getWindowingMode(); 159 } 160 161 /** Whether there is any {@link TaskFragmentContainer} below this Task. */ isEmpty()162 boolean isEmpty() { 163 return mContainers.isEmpty() && mFinishedContainer.isEmpty(); 164 } 165 166 /** Called when the activity is destroyed. */ onActivityDestroyed(@onNull IBinder activityToken)167 void onActivityDestroyed(@NonNull IBinder activityToken) { 168 for (TaskFragmentContainer container : mContainers) { 169 container.onActivityDestroyed(activityToken); 170 } 171 } 172 173 /** Removes the pending appeared activity from all TaskFragments in this Task. */ cleanupPendingAppearedActivity(@onNull IBinder activityToken)174 void cleanupPendingAppearedActivity(@NonNull IBinder activityToken) { 175 for (TaskFragmentContainer container : mContainers) { 176 container.removePendingAppearedActivity(activityToken); 177 } 178 } 179 180 @Nullable getTopNonFinishingTaskFragmentContainer()181 TaskFragmentContainer getTopNonFinishingTaskFragmentContainer() { 182 return getTopNonFinishingTaskFragmentContainer(true /* includePin */); 183 } 184 185 @Nullable getTopNonFinishingTaskFragmentContainer(boolean includePin)186 TaskFragmentContainer getTopNonFinishingTaskFragmentContainer(boolean includePin) { 187 for (int i = mContainers.size() - 1; i >= 0; i--) { 188 final TaskFragmentContainer container = mContainers.get(i); 189 if (!includePin && isTaskFragmentContainerPinned(container)) { 190 continue; 191 } 192 if (!container.isFinished()) { 193 return container; 194 } 195 } 196 return null; 197 } 198 199 /** Gets a non-finishing container below the given one. */ 200 @Nullable getNonFinishingTaskFragmentContainerBelow( @onNull TaskFragmentContainer current)201 TaskFragmentContainer getNonFinishingTaskFragmentContainerBelow( 202 @NonNull TaskFragmentContainer current) { 203 final int index = mContainers.indexOf(current); 204 for (int i = index - 1; i >= 0; i--) { 205 final TaskFragmentContainer container = mContainers.get(i); 206 if (!container.isFinished()) { 207 return container; 208 } 209 } 210 return null; 211 } 212 213 @Nullable getTopNonFinishingActivity()214 Activity getTopNonFinishingActivity() { 215 for (int i = mContainers.size() - 1; i >= 0; i--) { 216 final Activity activity = mContainers.get(i).getTopNonFinishingActivity(); 217 if (activity != null) { 218 return activity; 219 } 220 } 221 return null; 222 } 223 indexOf(@onNull TaskFragmentContainer child)224 int indexOf(@NonNull TaskFragmentContainer child) { 225 return mContainers.indexOf(child); 226 } 227 228 /** Whether the Task is in an intermediate state waiting for the server update.*/ isInIntermediateState()229 boolean isInIntermediateState() { 230 for (TaskFragmentContainer container : mContainers) { 231 if (container.isInIntermediateState()) { 232 // We are in an intermediate state to wait for server update on this TaskFragment. 233 return true; 234 } 235 } 236 return false; 237 } 238 239 /** 240 * Returns a list of {@link SplitContainer}. Do not modify the containers directly on the 241 * returned list. Use {@link #addSplitContainer} or {@link #removeSplitContainers} instead. 242 */ 243 @NonNull getSplitContainers()244 List<SplitContainer> getSplitContainers() { 245 return mSplitContainers; 246 } 247 addSplitContainer(@onNull SplitContainer splitContainer)248 void addSplitContainer(@NonNull SplitContainer splitContainer) { 249 if (splitContainer instanceof SplitPinContainer) { 250 mSplitPinContainer = (SplitPinContainer) splitContainer; 251 mSplitContainers.add(splitContainer); 252 return; 253 } 254 255 // Keeps the SplitPinContainer on the top of the list. 256 mSplitContainers.remove(mSplitPinContainer); 257 mSplitContainers.add(splitContainer); 258 if (mSplitPinContainer != null) { 259 mSplitContainers.add(mSplitPinContainer); 260 } 261 } 262 removeSplitContainers(@onNull List<SplitContainer> containers)263 void removeSplitContainers(@NonNull List<SplitContainer> containers) { 264 mSplitContainers.removeAll(containers); 265 } 266 removeSplitPinContainer()267 void removeSplitPinContainer() { 268 if (mSplitPinContainer == null) { 269 return; 270 } 271 272 final TaskFragmentContainer primaryContainer = mSplitPinContainer.getPrimaryContainer(); 273 final TaskFragmentContainer secondaryContainer = mSplitPinContainer.getSecondaryContainer(); 274 mSplitContainers.remove(mSplitPinContainer); 275 mSplitPinContainer = null; 276 277 // Remove the other SplitContainers that contains the unpinned container (unless it 278 // is the current top-most split-pair), since the state are no longer valid. 279 final List<SplitContainer> splitsToRemove = new ArrayList<>(); 280 for (SplitContainer splitContainer : mSplitContainers) { 281 if (splitContainer.getSecondaryContainer().equals(secondaryContainer) 282 && !splitContainer.getPrimaryContainer().equals(primaryContainer)) { 283 splitsToRemove.add(splitContainer); 284 } 285 } 286 removeSplitContainers(splitsToRemove); 287 } 288 289 @Nullable getSplitPinContainer()290 SplitPinContainer getSplitPinContainer() { 291 return mSplitPinContainer; 292 } 293 isTaskFragmentContainerPinned(@onNull TaskFragmentContainer taskFragmentContainer)294 boolean isTaskFragmentContainerPinned(@NonNull TaskFragmentContainer taskFragmentContainer) { 295 return mSplitPinContainer != null 296 && mSplitPinContainer.getSecondaryContainer() == taskFragmentContainer; 297 } 298 addTaskFragmentContainer(@onNull TaskFragmentContainer taskFragmentContainer)299 void addTaskFragmentContainer(@NonNull TaskFragmentContainer taskFragmentContainer) { 300 mContainers.add(taskFragmentContainer); 301 onTaskFragmentContainerUpdated(); 302 } 303 addTaskFragmentContainer(int index, @NonNull TaskFragmentContainer taskFragmentContainer)304 void addTaskFragmentContainer(int index, @NonNull TaskFragmentContainer taskFragmentContainer) { 305 mContainers.add(index, taskFragmentContainer); 306 onTaskFragmentContainerUpdated(); 307 } 308 removeTaskFragmentContainer(@onNull TaskFragmentContainer taskFragmentContainer)309 void removeTaskFragmentContainer(@NonNull TaskFragmentContainer taskFragmentContainer) { 310 mContainers.remove(taskFragmentContainer); 311 onTaskFragmentContainerUpdated(); 312 } 313 removeTaskFragmentContainers(@onNull List<TaskFragmentContainer> taskFragmentContainer)314 void removeTaskFragmentContainers(@NonNull List<TaskFragmentContainer> taskFragmentContainer) { 315 mContainers.removeAll(taskFragmentContainer); 316 onTaskFragmentContainerUpdated(); 317 } 318 clearTaskFragmentContainer()319 void clearTaskFragmentContainer() { 320 mContainers.clear(); 321 onTaskFragmentContainerUpdated(); 322 } 323 324 /** 325 * Returns a list of {@link TaskFragmentContainer}. Do not modify the containers directly on 326 * the returned list. Use {@link #addTaskFragmentContainer}, 327 * {@link #removeTaskFragmentContainer} or other related methods instead. 328 */ 329 @NonNull getTaskFragmentContainers()330 List<TaskFragmentContainer> getTaskFragmentContainers() { 331 return mContainers; 332 } 333 onTaskFragmentContainerUpdated()334 private void onTaskFragmentContainerUpdated() { 335 if (mSplitPinContainer == null) { 336 return; 337 } 338 339 final TaskFragmentContainer pinnedContainer = mSplitPinContainer.getSecondaryContainer(); 340 final int pinnedContainerIndex = mContainers.indexOf(pinnedContainer); 341 if (pinnedContainerIndex <= 0) { 342 removeSplitPinContainer(); 343 return; 344 } 345 346 // Ensure the pinned container is top-most. 347 if (pinnedContainerIndex != mContainers.size() - 1) { 348 mContainers.remove(pinnedContainer); 349 mContainers.add(pinnedContainer); 350 } 351 352 // Update the primary container adjacent to the pinned container if needed. 353 final TaskFragmentContainer adjacentContainer = 354 getNonFinishingTaskFragmentContainerBelow(pinnedContainer); 355 if (adjacentContainer == null) { 356 removeSplitPinContainer(); 357 } else if (mSplitPinContainer.getPrimaryContainer() != adjacentContainer) { 358 mSplitPinContainer.setPrimaryContainer(adjacentContainer); 359 } 360 } 361 362 /** Adds the descriptors of split states in this Task to {@code outSplitStates}. */ getSplitStates(@onNull List<SplitInfo> outSplitStates)363 void getSplitStates(@NonNull List<SplitInfo> outSplitStates) { 364 for (SplitContainer container : mSplitContainers) { 365 outSplitStates.add(container.toSplitInfo()); 366 } 367 } 368 369 /** A wrapper class which contains the information of {@link TaskContainer} */ 370 static final class TaskProperties { 371 private final int mDisplayId; 372 @NonNull 373 private final Configuration mConfiguration; 374 TaskProperties(int displayId, @NonNull Configuration configuration)375 TaskProperties(int displayId, @NonNull Configuration configuration) { 376 mDisplayId = displayId; 377 mConfiguration = configuration; 378 } 379 getDisplayId()380 int getDisplayId() { 381 return mDisplayId; 382 } 383 384 @NonNull getConfiguration()385 Configuration getConfiguration() { 386 return mConfiguration; 387 } 388 389 /** Translates the given absolute bounds to relative bounds in this Task coordinate. */ translateAbsoluteBoundsToRelativeBounds(@onNull Rect inOutBounds)390 void translateAbsoluteBoundsToRelativeBounds(@NonNull Rect inOutBounds) { 391 if (inOutBounds.isEmpty()) { 392 return; 393 } 394 final Rect taskBounds = mConfiguration.windowConfiguration.getBounds(); 395 inOutBounds.offset(-taskBounds.left, -taskBounds.top); 396 } 397 398 /** 399 * Obtains the {@link TaskProperties} for the task that the provided {@link Activity} is 400 * associated with. 401 * <p> 402 * Note that for most case, caller should use 403 * {@link SplitPresenter#getTaskProperties(Activity)} instead. This method is used before 404 * the {@code activity} goes into split. 405 * </p><p> 406 * If the {@link Activity} is in fullscreen, override 407 * {@link WindowConfiguration#getBounds()} with {@link WindowConfiguration#getMaxBounds()} 408 * in case the {@link Activity} is letterboxed. Otherwise, get the Task 409 * {@link Configuration} from the server side or use {@link Activity}'s 410 * {@link Configuration} as a fallback if the Task {@link Configuration} cannot be obtained. 411 */ 412 @NonNull getTaskPropertiesFromActivity(@onNull Activity activity)413 static TaskProperties getTaskPropertiesFromActivity(@NonNull Activity activity) { 414 final int displayId = activity.getDisplayId(); 415 // Use a copy of configuration because activity's configuration may be updated later, 416 // or we may get unexpected TaskContainer's configuration if Activity's configuration is 417 // updated. An example is Activity is going to be in split. 418 final Configuration activityConfig = new Configuration( 419 activity.getResources().getConfiguration()); 420 final WindowConfiguration windowConfiguration = activityConfig.windowConfiguration; 421 final int windowingMode = windowConfiguration.getWindowingMode(); 422 if (!inMultiWindowMode(windowingMode)) { 423 // Use the max bounds in fullscreen in case the Activity is letterboxed. 424 windowConfiguration.setBounds(windowConfiguration.getMaxBounds()); 425 return new TaskProperties(displayId, activityConfig); 426 } 427 final Configuration taskConfig = ActivityClient.getInstance() 428 .getTaskConfiguration(activity.getActivityToken()); 429 if (taskConfig == null) { 430 Log.w(TAG, "Could not obtain task configuration for activity:" + activity); 431 // Still report activity config if task config cannot be obtained from the server 432 // side. 433 return new TaskProperties(displayId, activityConfig); 434 } 435 return new TaskProperties(displayId, taskConfig); 436 } 437 } 438 } 439