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