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.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE;
20 import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_NONE;
21 
22 import android.os.IBinder;
23 import android.window.TaskFragmentOrganizer;
24 import android.window.TaskFragmentOrganizer.TaskFragmentTransitionType;
25 import android.window.WindowContainerTransaction;
26 
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 
32 /**
33  * Responsible for managing the current {@link WindowContainerTransaction} as a response to device
34  * state changes and app interactions.
35  *
36  * A typical use flow:
37  * 1. Call {@link #startNewTransaction} to start tracking the changes.
38  * 2. Use {@link TransactionRecord#setOriginType(int)} (int)} to record the type of operation that
39  *    will start a new transition on system server.
40  * 3. Use {@link #getCurrentTransactionRecord()} to get current {@link TransactionRecord} for
41  *    changes.
42  * 4. Call {@link TransactionRecord#apply(boolean)} to request the system server to apply changes in
43  *    the current {@link WindowContainerTransaction}, or call {@link TransactionRecord#abort()} to
44  *    dispose the current one.
45  *
46  * Note:
47  * There should be only one transaction at a time. The caller should not call
48  * {@link #startNewTransaction} again before calling {@link TransactionRecord#apply(boolean)} or
49  * {@link TransactionRecord#abort()} to the previous transaction.
50  */
51 class TransactionManager {
52 
53     @NonNull
54     private final TaskFragmentOrganizer mOrganizer;
55 
56     @Nullable
57     private TransactionRecord mCurrentTransaction;
58 
TransactionManager(@onNull TaskFragmentOrganizer organizer)59     TransactionManager(@NonNull TaskFragmentOrganizer organizer) {
60         mOrganizer = organizer;
61     }
62 
63     @NonNull
startNewTransaction()64     TransactionRecord startNewTransaction() {
65         return startNewTransaction(null /* taskFragmentTransactionToken */);
66     }
67 
68     /**
69      * Starts tracking the changes in a new {@link WindowContainerTransaction}. Caller can call
70      * {@link #getCurrentTransactionRecord()} later to continue adding change to the current
71      * transaction until {@link TransactionRecord#apply(boolean)} or
72      * {@link TransactionRecord#abort()} is called.
73      * @param taskFragmentTransactionToken  {@link android.window.TaskFragmentTransaction
74      *                                      #getTransactionToken()} if this is a response to a
75      *                                      {@link android.window.TaskFragmentTransaction}.
76      */
77     @NonNull
startNewTransaction(@ullable IBinder taskFragmentTransactionToken)78     TransactionRecord startNewTransaction(@Nullable IBinder taskFragmentTransactionToken) {
79         if (mCurrentTransaction != null) {
80             final TransactionRecord lastTransaction = mCurrentTransaction;
81             mCurrentTransaction = null;
82             throw new IllegalStateException(
83                     "The previous transaction:" + lastTransaction + " has not been applied or "
84                             + "aborted.");
85         }
86         mCurrentTransaction = new TransactionRecord(taskFragmentTransactionToken);
87         return mCurrentTransaction;
88     }
89 
90     /**
91      * Gets the current {@link TransactionRecord} started from {@link #startNewTransaction}.
92      */
93     @NonNull
getCurrentTransactionRecord()94     TransactionRecord getCurrentTransactionRecord() {
95         if (mCurrentTransaction == null) {
96             throw new IllegalStateException("startNewTransaction() is not invoked before calling"
97                     + " getCurrentTransactionRecord().");
98         }
99         return mCurrentTransaction;
100     }
101 
102     /** The current transaction. The manager should only handle one transaction at a time. */
103     class TransactionRecord {
104         /**
105          * {@link WindowContainerTransaction} containing the current change.
106          * @see #startNewTransaction(IBinder)
107          * @see #apply (boolean)
108          */
109         @NonNull
110         private final WindowContainerTransaction mTransaction = new WindowContainerTransaction();
111 
112         /**
113          * If the current transaction is a response to a
114          * {@link android.window.TaskFragmentTransaction}, this is the
115          * {@link android.window.TaskFragmentTransaction#getTransactionToken()}.
116          * @see #startNewTransaction(IBinder)
117          */
118         @Nullable
119         private final IBinder mTaskFragmentTransactionToken;
120 
121         /**
122          * To track of the origin type of the current {@link #mTransaction}. When
123          * {@link #apply (boolean)} to start a new transition, this is the type to request.
124          * @see #setOriginType(int)
125          * @see #getTransactionTransitionType()
126          */
127         @TaskFragmentTransitionType
128         private int mOriginType = TASK_FRAGMENT_TRANSIT_NONE;
129 
TransactionRecord(@ullable IBinder taskFragmentTransactionToken)130         TransactionRecord(@Nullable IBinder taskFragmentTransactionToken) {
131             mTaskFragmentTransactionToken = taskFragmentTransactionToken;
132         }
133 
134         @NonNull
getTransaction()135         WindowContainerTransaction getTransaction() {
136             ensureCurrentTransaction();
137             return mTransaction;
138         }
139 
140         /**
141          * Sets the {@link TaskFragmentTransitionType} that triggers this transaction. If there are
142          * multiple calls, only the first call will be respected as the "origin" type.
143          */
setOriginType(@askFragmentTransitionType int type)144         void setOriginType(@TaskFragmentTransitionType int type) {
145             ensureCurrentTransaction();
146             if (mOriginType != TASK_FRAGMENT_TRANSIT_NONE) {
147                 // Skip if the origin type has already been set.
148                 return;
149             }
150             mOriginType = type;
151         }
152 
153         /**
154          * Requests the system server to apply the current transaction started from
155          * {@link #startNewTransaction}.
156          * @param shouldApplyIndependently  If {@code true}, the {@link #mCurrentTransaction} will
157          *                                  request a new transition, which will be queued until the
158          *                                  sync engine is free if there is any other active sync.
159          *                                  If {@code false}, the {@link #startNewTransaction} will
160          *                                  be directly applied to the active sync.
161          */
apply(boolean shouldApplyIndependently)162         void apply(boolean shouldApplyIndependently) {
163             ensureCurrentTransaction();
164             if (mTaskFragmentTransactionToken != null) {
165                 // If this is a response to a TaskFragmentTransaction.
166                 mOrganizer.onTransactionHandled(mTaskFragmentTransactionToken, mTransaction,
167                         getTransactionTransitionType(), shouldApplyIndependently);
168             } else {
169                 mOrganizer.applyTransaction(mTransaction, getTransactionTransitionType(),
170                         shouldApplyIndependently);
171             }
172             dispose();
173         }
174 
175         /** Called when there is no need to {@link #apply(boolean)} the current transaction. */
abort()176         void abort() {
177             ensureCurrentTransaction();
178             dispose();
179         }
180 
dispose()181         private void dispose() {
182             TransactionManager.this.mCurrentTransaction = null;
183         }
184 
ensureCurrentTransaction()185         private void ensureCurrentTransaction() {
186             if (TransactionManager.this.mCurrentTransaction != this) {
187                 throw new IllegalStateException(
188                         "This transaction has already been apply() or abort().");
189             }
190         }
191 
192         /**
193          * Gets the {@link TaskFragmentTransitionType} that we will request transition with for the
194          * current {@link WindowContainerTransaction}.
195          */
196         @VisibleForTesting
197         @TaskFragmentTransitionType
getTransactionTransitionType()198         int getTransactionTransitionType() {
199             // Use TASK_FRAGMENT_TRANSIT_CHANGE as default if there is not opening/closing window.
200             return mOriginType != TASK_FRAGMENT_TRANSIT_NONE
201                     ? mOriginType
202                     : TASK_FRAGMENT_TRANSIT_CHANGE;
203         }
204 
205         @Override
206         @NonNull
toString()207         public String toString() {
208             return TransactionRecord.class.getSimpleName() + "{"
209                     + "token=" + mTaskFragmentTransactionToken
210                     + ", type=" + getTransactionTransitionType()
211                     + ", transaction=" + mTransaction
212                     + "}";
213         }
214     }
215 }
216