1 /*
2  * Copyright (C) 2020 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 com.android.server.wm;
18 
19 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
20 
21 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SYNC_ENGINE;
22 import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.os.Handler;
27 import android.os.Trace;
28 import android.util.ArraySet;
29 import android.util.Slog;
30 import android.view.SurfaceControl;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.internal.protolog.common.ProtoLog;
34 
35 import java.util.ArrayList;
36 
37 /**
38  * Utility class for collecting WindowContainers that will merge transactions.
39  * For example to use to synchronously resize all the children of a window container
40  *   1. Open a new sync set, and pass the listener that will be invoked
41  *        int id startSyncSet(TransactionReadyListener)
42  *      the returned ID will be eventually passed to the TransactionReadyListener in combination
43  *      with a set of WindowContainers that are ready, meaning onTransactionReady was called for
44  *      those WindowContainers. You also use it to refer to the operation in future steps.
45  *   2. Ask each child to participate:
46  *       addToSyncSet(int id, WindowContainer wc)
47  *      if the child thinks it will be affected by a configuration change (a.k.a. has a visible
48  *      window in its sub hierarchy, then we will increment a counter of expected callbacks
49  *      At this point the containers hierarchy will redirect pendingTransaction and sub hierarchy
50  *      updates in to the sync engine.
51  *   3. Apply your configuration changes to the window containers.
52  *   4. Tell the engine that the sync set is ready
53  *       setReady(int id)
54  *   5. If there were no sub windows anywhere in the hierarchy to wait on, then
55  *      transactionReady is immediately invoked, otherwise all the windows are poked
56  *      to redraw and to deliver a buffer to {@link WindowState#finishDrawing}.
57  *      Once all this drawing is complete, all the transactions will be merged and delivered
58  *      to TransactionReadyListener.
59  *
60  * This works primarily by setting-up state and then watching/waiting for the registered subtrees
61  * to enter into a "finished" state (either by receiving drawn content or by disappearing). This
62  * checks the subtrees during surface-placement.
63  *
64  * By default, all Syncs will be serialized (and it is an error to start one while another is
65  * active). However, a sync can be explicitly started in "parallel". This does not guarantee that
66  * it will run in parallel; however, it will run in parallel as long as it's watched hierarchy
67  * doesn't overlap with any other syncs' watched hierarchies.
68  *
69  * Currently, a sync that is started as "parallel" implicitly ignores the subtree below it's
70  * direct members unless those members are activities (WindowStates are considered "part of" the
71  * activity). This allows "stratified" parallelism where, eg, a sync that is only at Task-level
72  * can run in parallel with another sync that includes only the task's activities.
73  *
74  * If, at any time, a container is added to a parallel sync that *is* watched by another sync, it
75  * will be forced to serialize with it. This is done by adding a dependency. A sync will only
76  * finish if it has no active dependencies. At this point it is effectively not parallel anymore.
77  *
78  * To avoid dependency cycles, if a sync B ultimately depends on a sync A and a container is added
79  * to A which is watched by B, that container will, instead, be moved from B to A instead of
80  * creating a cyclic dependency.
81  *
82  * When syncs overlap, this will attempt to finish everything in the order they were started.
83  */
84 class BLASTSyncEngine {
85     private static final String TAG = "BLASTSyncEngine";
86 
87     /** No specific method. Used by override specifiers. */
88     public static final int METHOD_UNDEFINED = -1;
89 
90     /** No sync method. Apps will draw/present internally and just report. */
91     public static final int METHOD_NONE = 0;
92 
93     /** Sync with BLAST. Apps will draw and then send the buffer to be applied in sync. */
94     public static final int METHOD_BLAST = 1;
95 
96     interface TransactionReadyListener {
onTransactionReady(int mSyncId, SurfaceControl.Transaction transaction)97         void onTransactionReady(int mSyncId, SurfaceControl.Transaction transaction);
98     }
99 
100     /**
101      * Represents the desire to make a {@link BLASTSyncEngine.SyncGroup} while another is active.
102      *
103      * @see #queueSyncSet
104      */
105     private static class PendingSyncSet {
106         /** Called immediately when the {@link BLASTSyncEngine} is free. */
107         private Runnable mStartSync;
108 
109         /** Posted to the main handler after {@link #mStartSync} is called. */
110         private Runnable mApplySync;
111     }
112 
113     /**
114      * Holds state associated with a single synchronous set of operations.
115      */
116     class SyncGroup {
117         final int mSyncId;
118         int mSyncMethod = METHOD_BLAST;
119         final TransactionReadyListener mListener;
120         final Runnable mOnTimeout;
121         boolean mReady = false;
122         final ArraySet<WindowContainer> mRootMembers = new ArraySet<>();
123         private SurfaceControl.Transaction mOrphanTransaction = null;
124         private String mTraceName;
125 
126         private static final ArrayList<SyncGroup> NO_DEPENDENCIES = new ArrayList<>();
127 
128         /**
129          * When `true`, this SyncGroup will only wait for mRootMembers to draw; otherwise,
130          * it waits for the whole subtree(s) rooted at the mRootMembers.
131          */
132         boolean mIgnoreIndirectMembers = false;
133 
134         /** List of SyncGroups that must finish before this one can. */
135         @NonNull
136         ArrayList<SyncGroup> mDependencies = NO_DEPENDENCIES;
137 
SyncGroup(TransactionReadyListener listener, int id, String name)138         private SyncGroup(TransactionReadyListener listener, int id, String name) {
139             mSyncId = id;
140             mListener = listener;
141             mOnTimeout = () -> {
142                 Slog.w(TAG, "Sync group " + mSyncId + " timeout");
143                 synchronized (mWm.mGlobalLock) {
144                     onTimeout();
145                 }
146             };
147             if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
148                 mTraceName = name + "SyncGroupReady";
149                 Trace.asyncTraceBegin(TRACE_TAG_WINDOW_MANAGER, mTraceName, id);
150             }
151         }
152 
153         /**
154          * Gets a transaction to dump orphaned operations into. Orphaned operations are operations
155          * that were on the mSyncTransactions of "root" subtrees which have been removed during the
156          * sync period.
157          */
158         @NonNull
getOrphanTransaction()159         SurfaceControl.Transaction getOrphanTransaction() {
160             if (mOrphanTransaction == null) {
161                 // Lazy since this isn't common
162                 mOrphanTransaction = mWm.mTransactionFactory.get();
163             }
164             return mOrphanTransaction;
165         }
166 
167         /**
168          * Check if the sync-group ignores a particular container. This is used to allow syncs at
169          * different levels to run in parallel. The primary example is Recents while an activity
170          * sync is happening.
171          */
isIgnoring(WindowContainer wc)172         boolean isIgnoring(WindowContainer wc) {
173             // Some heuristics to avoid unnecessary work:
174             // 1. For now, require an explicit acknowledgement of potential "parallelism" across
175             //    hierarchy levels (horizontal).
176             if (!mIgnoreIndirectMembers) return false;
177             // 2. Don't check WindowStates since they are below the relevant abstraction level (
178             //    anything activity/token and above).
179             if (wc.asWindowState() != null) return false;
180             // Obviously, don't ignore anything that is directly part of this group.
181             return wc.mSyncGroup != this;
182         }
183 
184         /** @return `true` if it finished. */
tryFinish()185         private boolean tryFinish() {
186             if (!mReady) return false;
187             ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: onSurfacePlacement checking %s",
188                     mSyncId, mRootMembers);
189             if (!mDependencies.isEmpty()) {
190                 ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d:  Unfinished dependencies: %s",
191                         mSyncId, mDependencies);
192                 return false;
193             }
194             for (int i = mRootMembers.size() - 1; i >= 0; --i) {
195                 final WindowContainer wc = mRootMembers.valueAt(i);
196                 if (!wc.isSyncFinished(this)) {
197                     ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d:  Unfinished container: %s",
198                             mSyncId, wc);
199                     return false;
200                 }
201             }
202             finishNow();
203             return true;
204         }
205 
finishNow()206         private void finishNow() {
207             if (mTraceName != null) {
208                 Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, mTraceName, mSyncId);
209             }
210             ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Finished!", mSyncId);
211             SurfaceControl.Transaction merged = mWm.mTransactionFactory.get();
212             if (mOrphanTransaction != null) {
213                 merged.merge(mOrphanTransaction);
214             }
215             for (WindowContainer wc : mRootMembers) {
216                 wc.finishSync(merged, this, false /* cancel */);
217             }
218 
219             final ArraySet<WindowContainer> wcAwaitingCommit = new ArraySet<>();
220             for (WindowContainer wc : mRootMembers) {
221                 wc.waitForSyncTransactionCommit(wcAwaitingCommit);
222             }
223             class CommitCallback implements Runnable {
224                 // Can run a second time if the action completes after the timeout.
225                 boolean ran = false;
226                 public void onCommitted(SurfaceControl.Transaction t) {
227                     synchronized (mWm.mGlobalLock) {
228                         if (ran) {
229                             return;
230                         }
231                         mHandler.removeCallbacks(this);
232                         ran = true;
233                         for (WindowContainer wc : wcAwaitingCommit) {
234                             wc.onSyncTransactionCommitted(t);
235                         }
236                         t.apply();
237                         wcAwaitingCommit.clear();
238                     }
239                 }
240 
241                 // Called in timeout
242                 @Override
243                 public void run() {
244                     // Sometimes we get a trace, sometimes we get a bugreport without
245                     // a trace. Since these kind of ANRs can trigger such an issue,
246                     // try and ensure we will have some visibility in both cases.
247                     Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "onTransactionCommitTimeout");
248                     Slog.e(TAG, "WM sent Transaction to organized, but never received" +
249                            " commit callback. Application ANR likely to follow.");
250                     Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
251                     synchronized (mWm.mGlobalLock) {
252                         onCommitted(merged.mNativeObject != 0
253                                 ? merged : mWm.mTransactionFactory.get());
254                     }
255                 }
256             };
257             CommitCallback callback = new CommitCallback();
258             merged.addTransactionCommittedListener(Runnable::run,
259                     () -> callback.onCommitted(new SurfaceControl.Transaction()));
260             mHandler.postDelayed(callback, BLAST_TIMEOUT_DURATION);
261 
262             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "onTransactionReady");
263             mListener.onTransactionReady(mSyncId, merged);
264             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
265             mActiveSyncs.remove(this);
266             mHandler.removeCallbacks(mOnTimeout);
267 
268             // Immediately start the next pending sync-transaction if there is one.
269             if (mActiveSyncs.size() == 0 && !mPendingSyncSets.isEmpty()) {
270                 ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "PendingStartTransaction found");
271                 final PendingSyncSet pt = mPendingSyncSets.remove(0);
272                 pt.mStartSync.run();
273                 if (mActiveSyncs.size() == 0) {
274                     throw new IllegalStateException("Pending Sync Set didn't start a sync.");
275                 }
276                 // Post this so that the now-playing transition setup isn't interrupted.
277                 mHandler.post(() -> {
278                     synchronized (mWm.mGlobalLock) {
279                         pt.mApplySync.run();
280                     }
281                 });
282             }
283             // Notify idle listeners
284             for (int i = mOnIdleListeners.size() - 1; i >= 0; --i) {
285                 // If an idle listener adds a sync, though, then stop notifying.
286                 if (mActiveSyncs.size() > 0) break;
287                 mOnIdleListeners.get(i).run();
288             }
289         }
290 
291         /** returns true if readiness changed. */
setReady(boolean ready)292         private boolean setReady(boolean ready) {
293             if (mReady == ready) {
294                 return false;
295             }
296             ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Set ready %b", mSyncId, ready);
297             mReady = ready;
298             if (ready) {
299                 mWm.mWindowPlacerLocked.requestTraversal();
300             }
301             return true;
302         }
303 
addToSync(WindowContainer wc)304         private void addToSync(WindowContainer wc) {
305             if (mRootMembers.contains(wc)) {
306                 return;
307             }
308             ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Adding to group: %s", mSyncId, wc);
309             final SyncGroup dependency = wc.getSyncGroup();
310             if (dependency != null && dependency != this && !dependency.isIgnoring(wc)) {
311                 // This syncgroup now conflicts with another one, so the whole group now must
312                 // wait on the other group.
313                 Slog.w(TAG, "SyncGroup " + mSyncId + " conflicts with " + dependency.mSyncId
314                         + ": Making " + mSyncId + " depend on " + dependency.mSyncId);
315                 if (mDependencies.contains(dependency)) {
316                     // nothing, it's already a dependency.
317                 } else if (dependency.dependsOn(this)) {
318                     Slog.w(TAG, " Detected dependency cycle between " + mSyncId + " and "
319                             + dependency.mSyncId + ": Moving " + wc + " to " + mSyncId);
320                     // Since dependency already depends on this, make this now `wc`'s watcher
321                     if (wc.mSyncGroup == null) {
322                         wc.setSyncGroup(this);
323                     } else {
324                         // Explicit replacement.
325                         wc.mSyncGroup.mRootMembers.remove(wc);
326                         mRootMembers.add(wc);
327                         wc.mSyncGroup = this;
328                     }
329                 } else {
330                     if (mDependencies == NO_DEPENDENCIES) {
331                         mDependencies = new ArrayList<>();
332                     }
333                     mDependencies.add(dependency);
334                 }
335             } else {
336                 mRootMembers.add(wc);
337                 wc.setSyncGroup(this);
338             }
339             wc.prepareSync();
340             if (mReady) {
341                 mWm.mWindowPlacerLocked.requestTraversal();
342             }
343         }
344 
dependsOn(SyncGroup group)345         private boolean dependsOn(SyncGroup group) {
346             if (mDependencies.isEmpty()) return false;
347             // BFS search with membership check. We don't expect cycle here (since this is
348             // explicitly called to avoid cycles) but just to be safe.
349             final ArrayList<SyncGroup> fringe = mTmpFringe;
350             fringe.clear();
351             fringe.add(this);
352             for (int head = 0; head < fringe.size(); ++head) {
353                 final SyncGroup next = fringe.get(head);
354                 if (next == group) {
355                     fringe.clear();
356                     return true;
357                 }
358                 for (int i = 0; i < next.mDependencies.size(); ++i) {
359                     if (fringe.contains(next.mDependencies.get(i))) continue;
360                     fringe.add(next.mDependencies.get(i));
361                 }
362             }
363             fringe.clear();
364             return false;
365         }
366 
onCancelSync(WindowContainer wc)367         void onCancelSync(WindowContainer wc) {
368             mRootMembers.remove(wc);
369         }
370 
onTimeout()371         private void onTimeout() {
372             if (!mActiveSyncs.contains(this)) return;
373             boolean allFinished = true;
374             for (int i = mRootMembers.size() - 1; i >= 0; --i) {
375                 final WindowContainer<?> wc = mRootMembers.valueAt(i);
376                 if (!wc.isSyncFinished(this)) {
377                     allFinished = false;
378                     Slog.i(TAG, "Unfinished container: " + wc);
379                 }
380             }
381             for (int i = mDependencies.size() - 1; i >= 0; --i) {
382                 allFinished = false;
383                 Slog.i(TAG, "Unfinished dependency: " + mDependencies.get(i).mSyncId);
384             }
385             if (allFinished && !mReady) {
386                 Slog.w(TAG, "Sync group " + mSyncId + " timed-out because not ready. If you see "
387                         + "this, please file a bug.");
388             }
389             finishNow();
390             removeFromDependencies(this);
391         }
392     }
393 
394     private final WindowManagerService mWm;
395     private final Handler mHandler;
396     private int mNextSyncId = 0;
397 
398     /** Currently active syncs. Intentionally ordered by start time. */
399     private final ArrayList<SyncGroup> mActiveSyncs = new ArrayList<>();
400 
401     /**
402      * A queue of pending sync-sets waiting for their turn to run.
403      *
404      * @see #queueSyncSet
405      */
406     private final ArrayList<PendingSyncSet> mPendingSyncSets = new ArrayList<>();
407 
408     private final ArrayList<Runnable> mOnIdleListeners = new ArrayList<>();
409 
410     private final ArrayList<SyncGroup> mTmpFinishQueue = new ArrayList<>();
411     private final ArrayList<SyncGroup> mTmpFringe = new ArrayList<>();
412 
BLASTSyncEngine(WindowManagerService wms)413     BLASTSyncEngine(WindowManagerService wms) {
414         this(wms, wms.mH);
415     }
416 
417     @VisibleForTesting
BLASTSyncEngine(WindowManagerService wms, Handler mainHandler)418     BLASTSyncEngine(WindowManagerService wms, Handler mainHandler) {
419         mWm = wms;
420         mHandler = mainHandler;
421     }
422 
423     /**
424      * Prepares a {@link SyncGroup} that is not active yet. Caller must call {@link #startSyncSet}
425      * before calling {@link #addToSyncSet(int, WindowContainer)} on any {@link WindowContainer}.
426      */
prepareSyncSet(TransactionReadyListener listener, String name)427     SyncGroup prepareSyncSet(TransactionReadyListener listener, String name) {
428         return new SyncGroup(listener, mNextSyncId++, name);
429     }
430 
startSyncSet(TransactionReadyListener listener, long timeoutMs, String name, boolean parallel)431     int startSyncSet(TransactionReadyListener listener, long timeoutMs, String name,
432             boolean parallel) {
433         final SyncGroup s = prepareSyncSet(listener, name);
434         startSyncSet(s, timeoutMs, parallel);
435         return s.mSyncId;
436     }
437 
startSyncSet(SyncGroup s)438     void startSyncSet(SyncGroup s) {
439         startSyncSet(s, BLAST_TIMEOUT_DURATION, false /* parallel */);
440     }
441 
startSyncSet(SyncGroup s, long timeoutMs, boolean parallel)442     void startSyncSet(SyncGroup s, long timeoutMs, boolean parallel) {
443         final boolean alreadyRunning = mActiveSyncs.size() > 0;
444         if (!parallel && alreadyRunning) {
445             // We only support overlapping syncs when explicitly declared `parallel`.
446             Slog.e(TAG, "SyncGroup " + s.mSyncId
447                     + ": Started when there is other active SyncGroup");
448         }
449         mActiveSyncs.add(s);
450         // For now, parallel implies this.
451         s.mIgnoreIndirectMembers = parallel;
452         ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Started %sfor listener: %s",
453                 s.mSyncId, (parallel && alreadyRunning ? "(in parallel) " : ""), s.mListener);
454         scheduleTimeout(s, timeoutMs);
455     }
456 
457     @Nullable
getSyncSet(int id)458     SyncGroup getSyncSet(int id) {
459         for (int i = 0; i < mActiveSyncs.size(); ++i) {
460             if (mActiveSyncs.get(i).mSyncId != id) continue;
461             return mActiveSyncs.get(i);
462         }
463         return null;
464     }
465 
hasActiveSync()466     boolean hasActiveSync() {
467         return mActiveSyncs.size() != 0;
468     }
469 
470     @VisibleForTesting
scheduleTimeout(SyncGroup s, long timeoutMs)471     void scheduleTimeout(SyncGroup s, long timeoutMs) {
472         mHandler.postDelayed(s.mOnTimeout, timeoutMs);
473     }
474 
addToSyncSet(int id, WindowContainer wc)475     void addToSyncSet(int id, WindowContainer wc) {
476         getSyncGroup(id).addToSync(wc);
477     }
478 
setSyncMethod(int id, int method)479     void setSyncMethod(int id, int method) {
480         final SyncGroup syncGroup = getSyncGroup(id);
481         if (!syncGroup.mRootMembers.isEmpty()) {
482             throw new IllegalStateException(
483                     "Not allow to change sync method after adding group member, id=" + id);
484         }
485         syncGroup.mSyncMethod = method;
486     }
487 
setReady(int id, boolean ready)488     boolean setReady(int id, boolean ready) {
489         return getSyncGroup(id).setReady(ready);
490     }
491 
setReady(int id)492     void setReady(int id) {
493         setReady(id, true);
494     }
495 
isReady(int id)496     boolean isReady(int id) {
497         return getSyncGroup(id).mReady;
498     }
499 
500     /**
501      * Aborts the sync (ie. it doesn't wait for ready or anything to finish)
502      */
abort(int id)503     void abort(int id) {
504         final SyncGroup group = getSyncGroup(id);
505         group.finishNow();
506         removeFromDependencies(group);
507     }
508 
getSyncGroup(int id)509     private SyncGroup getSyncGroup(int id) {
510         final SyncGroup syncGroup = getSyncSet(id);
511         if (syncGroup == null) {
512             throw new IllegalStateException("SyncGroup is not started yet id=" + id);
513         }
514         return syncGroup;
515     }
516 
517     /**
518      * Just removes `group` from any dependency lists. Does not try to evaluate anything. However,
519      * it will schedule traversals if any groups were changed in a way that could make them ready.
520      */
removeFromDependencies(SyncGroup group)521     private void removeFromDependencies(SyncGroup group) {
522         boolean anyChange = false;
523         for (int i = 0; i < mActiveSyncs.size(); ++i) {
524             final SyncGroup active = mActiveSyncs.get(i);
525             if (!active.mDependencies.remove(group)) continue;
526             if (!active.mDependencies.isEmpty()) continue;
527             anyChange = true;
528         }
529         if (!anyChange) return;
530         mWm.mWindowPlacerLocked.requestTraversal();
531     }
532 
onSurfacePlacement()533     void onSurfacePlacement() {
534         if (mActiveSyncs.isEmpty()) return;
535         // queue in-order since we want interdependent syncs to become ready in the same order they
536         // started in.
537         mTmpFinishQueue.addAll(mActiveSyncs);
538         // There shouldn't be any dependency cycles or duplicates, but add an upper-bound just
539         // in case. Assuming absolute worst case, each visit will try and revisit everything
540         // before it, so n + (n-1) + (n-2) ... = (n+1)*n/2
541         int visitBounds = ((mActiveSyncs.size() + 1) * mActiveSyncs.size()) / 2;
542         while (!mTmpFinishQueue.isEmpty()) {
543             if (visitBounds <= 0) {
544                 Slog.e(TAG, "Trying to finish more syncs than theoretically possible. This "
545                         + "should never happen. Most likely a dependency cycle wasn't detected.");
546             }
547             --visitBounds;
548             final SyncGroup group = mTmpFinishQueue.remove(0);
549             final int grpIdx = mActiveSyncs.indexOf(group);
550             // Skip if it's already finished:
551             if (grpIdx < 0) continue;
552             if (!group.tryFinish()) continue;
553             // Finished, so update dependencies of any prior groups and retry if unblocked.
554             int insertAt = 0;
555             for (int i = 0; i < mActiveSyncs.size(); ++i) {
556                 final SyncGroup active = mActiveSyncs.get(i);
557                 if (!active.mDependencies.remove(group)) continue;
558                 // Anything afterwards is already in queue.
559                 if (i >= grpIdx) continue;
560                 if (!active.mDependencies.isEmpty()) continue;
561                 // `active` became unblocked so it can finish, since it started earlier, it should
562                 // be checked next to maintain order.
563                 mTmpFinishQueue.add(insertAt, mActiveSyncs.get(i));
564                 insertAt += 1;
565             }
566         }
567     }
568 
569     /** Only use this for tests! */
tryFinishForTest(int syncId)570     void tryFinishForTest(int syncId) {
571         getSyncSet(syncId).tryFinish();
572     }
573 
574     /**
575      * Queues a sync operation onto this engine. It will wait until any current/prior sync-sets
576      * have finished to run. This is needed right now because currently {@link BLASTSyncEngine}
577      * only supports 1 sync at a time.
578      *
579      * Code-paths should avoid using this unless absolutely necessary. Usually, we use this for
580      * difficult edge-cases that we hope to clean-up later.
581      *
582      * @param startSync will be called immediately when the {@link BLASTSyncEngine} is free to
583      *                  "reserve" the {@link BLASTSyncEngine} by calling one of the
584      *                  {@link BLASTSyncEngine#startSyncSet} variants.
585      * @param applySync will be posted to the main handler after {@code startSync} has been
586      *                  called. This is posted so that it doesn't interrupt any clean-up for the
587      *                  prior sync-set.
588      */
queueSyncSet(@onNull Runnable startSync, @NonNull Runnable applySync)589     void queueSyncSet(@NonNull Runnable startSync, @NonNull Runnable applySync) {
590         final PendingSyncSet pt = new PendingSyncSet();
591         pt.mStartSync = startSync;
592         pt.mApplySync = applySync;
593         mPendingSyncSets.add(pt);
594     }
595 
596     /** @return {@code true} if there are any sync-sets waiting to start. */
hasPendingSyncSets()597     boolean hasPendingSyncSets() {
598         return !mPendingSyncSets.isEmpty();
599     }
600 
addOnIdleListener(Runnable onIdleListener)601     void addOnIdleListener(Runnable onIdleListener) {
602         mOnIdleListeners.add(onIdleListener);
603     }
604 }
605