1 /* 2 * Copyright (C) 2021 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 android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.Activity; 22 import android.app.ActivityThread; 23 import android.graphics.Rect; 24 import android.os.Binder; 25 import android.os.IBinder; 26 import android.window.TaskFragmentInfo; 27 import android.window.WindowContainerTransaction; 28 29 import java.util.ArrayList; 30 import java.util.Iterator; 31 import java.util.List; 32 33 /** 34 * Client-side container for a stack of activities. Corresponds to an instance of TaskFragment 35 * on the server side. 36 */ 37 class TaskFragmentContainer { 38 /** 39 * Client-created token that uniquely identifies the task fragment container instance. 40 */ 41 @NonNull 42 private final IBinder mToken; 43 44 /** 45 * Server-provided task fragment information. 46 */ 47 private TaskFragmentInfo mInfo; 48 49 /** 50 * Activities that are being reparented or being started to this container, but haven't been 51 * added to {@link #mInfo} yet. 52 */ 53 private final ArrayList<Activity> mPendingAppearedActivities = new ArrayList<>(); 54 55 /** Containers that are dependent on this one and should be completely destroyed on exit. */ 56 private final List<TaskFragmentContainer> mContainersToFinishOnExit = 57 new ArrayList<>(); 58 59 /** Individual associated activities in different containers that should be finished on exit. */ 60 private final List<Activity> mActivitiesToFinishOnExit = new ArrayList<>(); 61 62 /** Indicates whether the container was cleaned up after the last activity was removed. */ 63 private boolean mIsFinished; 64 65 /** 66 * Bounds that were requested last via {@link android.window.WindowContainerTransaction}. 67 */ 68 private final Rect mLastRequestedBounds = new Rect(); 69 70 /** 71 * Creates a container with an existing activity that will be re-parented to it in a window 72 * container transaction. 73 */ TaskFragmentContainer(@ullable Activity activity)74 TaskFragmentContainer(@Nullable Activity activity) { 75 mToken = new Binder("TaskFragmentContainer"); 76 if (activity != null) { 77 addPendingAppearedActivity(activity); 78 } 79 } 80 81 /** 82 * Returns the client-created token that uniquely identifies this container. 83 */ 84 @NonNull getTaskFragmentToken()85 IBinder getTaskFragmentToken() { 86 return mToken; 87 } 88 89 /** List of activities that belong to this container and live in this process. */ 90 @NonNull collectActivities()91 List<Activity> collectActivities() { 92 // Add the re-parenting activity, in case the server has not yet reported the task 93 // fragment info update with it placed in this container. We still want to apply rules 94 // in this intermediate state. 95 List<Activity> allActivities = new ArrayList<>(); 96 if (!mPendingAppearedActivities.isEmpty()) { 97 allActivities.addAll(mPendingAppearedActivities); 98 } 99 // Add activities reported from the server. 100 if (mInfo == null) { 101 return allActivities; 102 } 103 ActivityThread activityThread = ActivityThread.currentActivityThread(); 104 for (IBinder token : mInfo.getActivities()) { 105 Activity activity = activityThread.getActivity(token); 106 if (activity != null && !activity.isFinishing() && !allActivities.contains(activity)) { 107 allActivities.add(activity); 108 } 109 } 110 return allActivities; 111 } 112 toActivityStack()113 ActivityStack toActivityStack() { 114 return new ActivityStack(collectActivities(), mInfo.getRunningActivityCount() == 0); 115 } 116 addPendingAppearedActivity(@onNull Activity pendingAppearedActivity)117 void addPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { 118 mPendingAppearedActivities.add(pendingAppearedActivity); 119 } 120 hasActivity(@onNull IBinder token)121 boolean hasActivity(@NonNull IBinder token) { 122 if (mInfo != null && mInfo.getActivities().contains(token)) { 123 return true; 124 } 125 for (Activity activity : mPendingAppearedActivities) { 126 if (activity.getActivityToken().equals(token)) { 127 return true; 128 } 129 } 130 return false; 131 } 132 getRunningActivityCount()133 int getRunningActivityCount() { 134 int count = mPendingAppearedActivities.size(); 135 if (mInfo != null) { 136 count += mInfo.getRunningActivityCount(); 137 } 138 return count; 139 } 140 141 @Nullable getInfo()142 TaskFragmentInfo getInfo() { 143 return mInfo; 144 } 145 setInfo(@onNull TaskFragmentInfo info)146 void setInfo(@NonNull TaskFragmentInfo info) { 147 mInfo = info; 148 if (mInfo == null || mPendingAppearedActivities.isEmpty()) { 149 return; 150 } 151 // Cleanup activities that were being re-parented 152 List<IBinder> infoActivities = mInfo.getActivities(); 153 for (int i = mPendingAppearedActivities.size() - 1; i >= 0; --i) { 154 final Activity activity = mPendingAppearedActivities.get(i); 155 if (infoActivities.contains(activity.getActivityToken())) { 156 mPendingAppearedActivities.remove(i); 157 } 158 } 159 } 160 161 @Nullable getTopNonFinishingActivity()162 Activity getTopNonFinishingActivity() { 163 List<Activity> activities = collectActivities(); 164 if (activities.isEmpty()) { 165 return null; 166 } 167 int i = activities.size() - 1; 168 while (i >= 0 && activities.get(i).isFinishing()) { 169 i--; 170 } 171 return i >= 0 ? activities.get(i) : null; 172 } 173 isEmpty()174 boolean isEmpty() { 175 return mPendingAppearedActivities.isEmpty() && (mInfo == null || mInfo.isEmpty()); 176 } 177 178 /** 179 * Adds a container that should be finished when this container is finished. 180 */ addContainerToFinishOnExit(@onNull TaskFragmentContainer containerToFinish)181 void addContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToFinish) { 182 mContainersToFinishOnExit.add(containerToFinish); 183 } 184 185 /** 186 * Adds an activity that should be finished when this container is finished. 187 */ addActivityToFinishOnExit(@onNull Activity activityToFinish)188 void addActivityToFinishOnExit(@NonNull Activity activityToFinish) { 189 mActivitiesToFinishOnExit.add(activityToFinish); 190 } 191 192 /** 193 * Removes all activities that belong to this process and finishes other containers/activities 194 * configured to finish together. 195 */ finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, @NonNull WindowContainerTransaction wct, @NonNull SplitController controller)196 void finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, 197 @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) { 198 if (!mIsFinished) { 199 mIsFinished = true; 200 finishActivities(shouldFinishDependent, presenter, wct, controller); 201 } 202 203 if (mInfo == null) { 204 // Defer removal the container and wait until TaskFragment appeared. 205 return; 206 } 207 208 // Cleanup the visuals 209 presenter.deleteTaskFragment(wct, getTaskFragmentToken()); 210 // Cleanup the records 211 controller.removeContainer(this); 212 // Clean up task fragment information 213 mInfo = null; 214 } 215 finishActivities(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, @NonNull WindowContainerTransaction wct, @NonNull SplitController controller)216 private void finishActivities(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, 217 @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) { 218 // Finish own activities 219 for (Activity activity : collectActivities()) { 220 if (!activity.isFinishing()) { 221 activity.finish(); 222 } 223 } 224 225 if (!shouldFinishDependent) { 226 return; 227 } 228 229 // Finish dependent containers 230 for (TaskFragmentContainer container : mContainersToFinishOnExit) { 231 if (controller.shouldRetainAssociatedContainer(this, container)) { 232 continue; 233 } 234 container.finish(true /* shouldFinishDependent */, presenter, 235 wct, controller); 236 } 237 mContainersToFinishOnExit.clear(); 238 239 // Finish associated activities 240 for (Activity activity : mActivitiesToFinishOnExit) { 241 if (controller.shouldRetainAssociatedActivity(this, activity)) { 242 continue; 243 } 244 activity.finish(); 245 } 246 mActivitiesToFinishOnExit.clear(); 247 248 // Finish activities that were being re-parented to this container. 249 for (Activity activity : mPendingAppearedActivities) { 250 activity.finish(); 251 } 252 mPendingAppearedActivities.clear(); 253 } 254 isFinished()255 boolean isFinished() { 256 return mIsFinished; 257 } 258 259 /** 260 * Checks if last requested bounds are equal to the provided value. 261 */ areLastRequestedBoundsEqual(@ullable Rect bounds)262 boolean areLastRequestedBoundsEqual(@Nullable Rect bounds) { 263 return (bounds == null && mLastRequestedBounds.isEmpty()) 264 || mLastRequestedBounds.equals(bounds); 265 } 266 267 /** 268 * Updates the last requested bounds. 269 */ setLastRequestedBounds(@ullable Rect bounds)270 void setLastRequestedBounds(@Nullable Rect bounds) { 271 if (bounds == null) { 272 mLastRequestedBounds.setEmpty(); 273 } else { 274 mLastRequestedBounds.set(bounds); 275 } 276 } 277 278 @Override toString()279 public String toString() { 280 return toString(true /* includeContainersToFinishOnExit */); 281 } 282 283 /** 284 * @return string for this TaskFragmentContainer and includes containers to finish on exit 285 * based on {@code includeContainersToFinishOnExit}. If containers to finish on exit are always 286 * included in the string, then calling {@link #toString()} on a container that mutually 287 * finishes with another container would cause a stack overflow. 288 */ toString(boolean includeContainersToFinishOnExit)289 private String toString(boolean includeContainersToFinishOnExit) { 290 return "TaskFragmentContainer{" 291 + " token=" + mToken 292 + " info=" + mInfo 293 + " topNonFinishingActivity=" + getTopNonFinishingActivity() 294 + " pendingAppearedActivities=" + mPendingAppearedActivities 295 + (includeContainersToFinishOnExit ? " containersToFinishOnExit=" 296 + containersToFinishOnExitToString() : "") 297 + " activitiesToFinishOnExit=" + mActivitiesToFinishOnExit 298 + " isFinished=" + mIsFinished 299 + " lastRequestedBounds=" + mLastRequestedBounds 300 + "}"; 301 } 302 containersToFinishOnExitToString()303 private String containersToFinishOnExitToString() { 304 StringBuilder sb = new StringBuilder("["); 305 Iterator<TaskFragmentContainer> containerIterator = mContainersToFinishOnExit.iterator(); 306 while (containerIterator.hasNext()) { 307 sb.append(containerIterator.next().toString( 308 false /* includeContainersToFinishOnExit */)); 309 if (containerIterator.hasNext()) { 310 sb.append(", "); 311 } 312 } 313 return sb.append("]").toString(); 314 } 315 } 316