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