blob: 4b15bb187035e783d047c3d7a2a6d14d8c1a1fe9 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.window.extensions.embedding;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.app.WindowConfiguration.inMultiWindowMode;
import android.app.Activity;
import android.app.ActivityClient;
import android.app.WindowConfiguration;
import android.app.WindowConfiguration.WindowingMode;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.IBinder;
import android.util.ArraySet;
import android.util.Log;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentParentInfo;
import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/** Represents TaskFragments and split pairs below a Task. */
class TaskContainer {
private static final String TAG = TaskContainer.class.getSimpleName();
/** The unique task id. */
private final int mTaskId;
/** Active TaskFragments in this Task. */
@NonNull
final List<TaskFragmentContainer> mContainers = new ArrayList<>();
/** Active split pairs in this Task. */
@NonNull
final List<SplitContainer> mSplitContainers = new ArrayList<>();
@NonNull
private final Configuration mConfiguration;
private int mDisplayId;
private boolean mIsVisible;
/**
* TaskFragments that the organizer has requested to be closed. They should be removed when
* the organizer receives
* {@link SplitController#onTaskFragmentVanished(WindowContainerTransaction, TaskFragmentInfo)}
* event for them.
*/
final Set<IBinder> mFinishedContainer = new ArraySet<>();
/**
* The {@link TaskContainer} constructor
*
* @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with
* {@code activityInTask}.
* @param activityInTask The {@link Activity} in the Task with {@code taskId}. It is used to
* initialize the {@link TaskContainer} properties.
*
*/
TaskContainer(int taskId, @NonNull Activity activityInTask) {
if (taskId == INVALID_TASK_ID) {
throw new IllegalArgumentException("Invalid Task id");
}
mTaskId = taskId;
final TaskProperties taskProperties = TaskProperties
.getTaskPropertiesFromActivity(activityInTask);
mConfiguration = taskProperties.getConfiguration();
mDisplayId = taskProperties.getDisplayId();
// Note that it is always called when there's a new Activity is started, which implies
// the host task is visible.
mIsVisible = true;
}
int getTaskId() {
return mTaskId;
}
int getDisplayId() {
return mDisplayId;
}
boolean isVisible() {
return mIsVisible;
}
@NonNull
TaskProperties getTaskProperties() {
return new TaskProperties(mDisplayId, mConfiguration);
}
void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) {
mConfiguration.setTo(info.getConfiguration());
mDisplayId = info.getDisplayId();
mIsVisible = info.isVisible();
}
/**
* Returns the windowing mode for the TaskFragments below this Task, which should be split with
* other TaskFragments.
*
* @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when
* the pair of TaskFragments are stacked due to the limited space.
*/
@WindowingMode
int getWindowingModeForSplitTaskFragment(@Nullable Rect taskFragmentBounds) {
// Only set to multi-windowing mode if the pair are showing side-by-side. Otherwise, it
// will be set to UNDEFINED which will then inherit the Task windowing mode.
if (taskFragmentBounds == null || taskFragmentBounds.isEmpty() || isInPictureInPicture()) {
return WINDOWING_MODE_UNDEFINED;
}
// We use WINDOWING_MODE_MULTI_WINDOW when the Task is fullscreen.
// However, when the Task is in other multi windowing mode, such as Freeform, we need to
// have the activity windowing mode to match the Task, otherwise things like
// DecorCaptionView won't work correctly. As a result, have the TaskFragment to be in the
// Task windowing mode if the Task is in multi window.
// TODO we won't need this anymore after we migrate Freeform caption to WM Shell.
return isInMultiWindow() ? getWindowingMode() : WINDOWING_MODE_MULTI_WINDOW;
}
boolean isInPictureInPicture() {
return getWindowingMode() == WINDOWING_MODE_PINNED;
}
boolean isInMultiWindow() {
return WindowConfiguration.inMultiWindowMode(getWindowingMode());
}
@WindowingMode
private int getWindowingMode() {
return mConfiguration.windowConfiguration.getWindowingMode();
}
/** Whether there is any {@link TaskFragmentContainer} below this Task. */
boolean isEmpty() {
return mContainers.isEmpty() && mFinishedContainer.isEmpty();
}
/** Called when the activity is destroyed. */
void onActivityDestroyed(@NonNull IBinder activityToken) {
for (TaskFragmentContainer container : mContainers) {
container.onActivityDestroyed(activityToken);
}
}
/** Removes the pending appeared activity from all TaskFragments in this Task. */
void cleanupPendingAppearedActivity(@NonNull IBinder activityToken) {
for (TaskFragmentContainer container : mContainers) {
container.removePendingAppearedActivity(activityToken);
}
}
@Nullable
TaskFragmentContainer getTopTaskFragmentContainer() {
if (mContainers.isEmpty()) {
return null;
}
return mContainers.get(mContainers.size() - 1);
}
@Nullable
Activity getTopNonFinishingActivity() {
for (int i = mContainers.size() - 1; i >= 0; i--) {
final Activity activity = mContainers.get(i).getTopNonFinishingActivity();
if (activity != null) {
return activity;
}
}
return null;
}
int indexOf(@NonNull TaskFragmentContainer child) {
return mContainers.indexOf(child);
}
/** Whether the Task is in an intermediate state waiting for the server update.*/
boolean isInIntermediateState() {
for (TaskFragmentContainer container : mContainers) {
if (container.isInIntermediateState()) {
// We are in an intermediate state to wait for server update on this TaskFragment.
return true;
}
}
return false;
}
/** Adds the descriptors of split states in this Task to {@code outSplitStates}. */
void getSplitStates(@NonNull List<SplitInfo> outSplitStates) {
for (SplitContainer container : mSplitContainers) {
outSplitStates.add(container.toSplitInfo());
}
}
/** A wrapper class which contains the information of {@link TaskContainer} */
static final class TaskProperties {
private final int mDisplayId;
@NonNull
private final Configuration mConfiguration;
TaskProperties(int displayId, @NonNull Configuration configuration) {
mDisplayId = displayId;
mConfiguration = configuration;
}
int getDisplayId() {
return mDisplayId;
}
@NonNull
Configuration getConfiguration() {
return mConfiguration;
}
/** Translates the given absolute bounds to relative bounds in this Task coordinate. */
void translateAbsoluteBoundsToRelativeBounds(@NonNull Rect inOutBounds) {
if (inOutBounds.isEmpty()) {
return;
}
final Rect taskBounds = mConfiguration.windowConfiguration.getBounds();
inOutBounds.offset(-taskBounds.left, -taskBounds.top);
}
/**
* Obtains the {@link TaskProperties} for the task that the provided {@link Activity} is
* associated with.
* <p>
* Note that for most case, caller should use
* {@link SplitPresenter#getTaskProperties(Activity)} instead. This method is used before
* the {@code activity} goes into split.
* </p><p>
* If the {@link Activity} is in fullscreen, override
* {@link WindowConfiguration#getBounds()} with {@link WindowConfiguration#getMaxBounds()}
* in case the {@link Activity} is letterboxed. Otherwise, get the Task
* {@link Configuration} from the server side or use {@link Activity}'s
* {@link Configuration} as a fallback if the Task {@link Configuration} cannot be obtained.
*/
@NonNull
static TaskProperties getTaskPropertiesFromActivity(@NonNull Activity activity) {
final int displayId = activity.getDisplayId();
// Use a copy of configuration because activity's configuration may be updated later,
// or we may get unexpected TaskContainer's configuration if Activity's configuration is
// updated. An example is Activity is going to be in split.
final Configuration activityConfig = new Configuration(
activity.getResources().getConfiguration());
final WindowConfiguration windowConfiguration = activityConfig.windowConfiguration;
final int windowingMode = windowConfiguration.getWindowingMode();
if (!inMultiWindowMode(windowingMode)) {
// Use the max bounds in fullscreen in case the Activity is letterboxed.
windowConfiguration.setBounds(windowConfiguration.getMaxBounds());
return new TaskProperties(displayId, activityConfig);
}
final Configuration taskConfig = ActivityClient.getInstance()
.getTaskConfiguration(activity.getActivityToken());
if (taskConfig == null) {
Log.w(TAG, "Could not obtain task configuration for activity:" + activity);
// Still report activity config if task config cannot be obtained from the server
// side.
return new TaskProperties(displayId, activityConfig);
}
return new TaskProperties(displayId, taskConfig);
}
}
}