1 /* 2 * Copyright 2019 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.camera.core.impl; 18 19 import android.graphics.ImageFormat; 20 import android.util.Log; 21 import android.util.Size; 22 import android.view.Surface; 23 24 import androidx.annotation.GuardedBy; 25 import androidx.annotation.RestrictTo; 26 import androidx.annotation.RestrictTo.Scope; 27 import androidx.annotation.VisibleForTesting; 28 import androidx.camera.core.Logger; 29 import androidx.camera.core.impl.utils.executor.CameraXExecutors; 30 import androidx.camera.core.impl.utils.futures.Futures; 31 import androidx.camera.core.processing.SurfaceEdge; 32 import androidx.concurrent.futures.CallbackToFutureAdapter; 33 34 import com.google.common.util.concurrent.ListenableFuture; 35 36 import org.jspecify.annotations.NonNull; 37 import org.jspecify.annotations.Nullable; 38 39 import java.util.concurrent.atomic.AtomicInteger; 40 41 /** 42 * A class for creating and tracking use of a {@link Surface} in an asynchronous manner. 43 * 44 * <p>Once the deferrable surface has been closed via {@link #close()} and is no longer in 45 * use ({@link #decrementUseCount()} has been called equal to the number of times to 46 * {@link #incrementUseCount()}), then the surface is considered terminated. 47 * 48 * <p>Resources managed by this class can be safely cleaned up upon completion of the 49 * {@link ListenableFuture} returned by {@link #getTerminationFuture()}. 50 */ 51 public abstract class DeferrableSurface { 52 53 /** 54 * The exception that is returned by the ListenableFuture of {@link #getSurface()} if the 55 * deferrable surface is unable to produce a {@link Surface}. 56 */ 57 public static final class SurfaceUnavailableException extends Exception { SurfaceUnavailableException(@onNull String message)58 public SurfaceUnavailableException(@NonNull String message) { 59 super(message); 60 } 61 } 62 63 /** 64 * The exception that is returned by the ListenableFuture of {@link #getSurface()} if the 65 * {@link Surface} backing the DeferrableSurface has already been closed. 66 */ 67 @RestrictTo(Scope.LIBRARY_GROUP) 68 public static final class SurfaceClosedException extends Exception { 69 DeferrableSurface mDeferrableSurface; 70 SurfaceClosedException(@onNull String s, @NonNull DeferrableSurface surface)71 public SurfaceClosedException(@NonNull String s, @NonNull DeferrableSurface surface) { 72 super(s); 73 mDeferrableSurface = surface; 74 } 75 76 /** 77 * Returns the {@link DeferrableSurface} that generated the exception. 78 * 79 * <p>The deferrable surface will already be closed. 80 */ getDeferrableSurface()81 public @NonNull DeferrableSurface getDeferrableSurface() { 82 return mDeferrableSurface; 83 } 84 } 85 86 // The size of the surface is not defined. 87 public static final Size SIZE_UNDEFINED = new Size(0, 0); 88 89 private static final String TAG = "DeferrableSurface"; 90 private static final boolean DEBUG = Logger.isDebugEnabled(TAG); 91 92 // Debug only, used to track total count of surfaces in use. 93 private static final AtomicInteger USED_COUNT = new AtomicInteger(0); 94 // Debug only, used to track total count of surfaces, including those not in use. Will be 95 // decremented once surface is cleaned. 96 private static final AtomicInteger TOTAL_COUNT = new AtomicInteger(0); 97 98 // Lock used for accessing states. 99 private final Object mLock = new Object(); 100 101 // The use count. 102 @GuardedBy("mLock") 103 private int mUseCount = 0; 104 105 @GuardedBy("mLock") 106 private boolean mClosed = false; 107 108 @GuardedBy("mLock") 109 private CallbackToFutureAdapter.Completer<Void> mTerminationCompleter; 110 private final ListenableFuture<Void> mTerminationFuture; 111 112 @GuardedBy("mLock") 113 private CallbackToFutureAdapter.Completer<Void> mCloseCompleter; 114 private final ListenableFuture<Void> mCloseFuture; 115 116 private final @NonNull Size mPrescribedSize; 117 private final int mPrescribedStreamFormat; 118 @Nullable Class<?> mContainerClass; 119 120 /** 121 * Creates a new DeferrableSurface which has no use count. 122 */ DeferrableSurface()123 public DeferrableSurface() { 124 this(SIZE_UNDEFINED, ImageFormat.UNKNOWN); 125 } 126 127 /** 128 * Creates a new DeferrableSurface which has no use count. 129 * 130 * @param size the {@link Size} of the surface 131 * @param format the stream configuration format that the provided Surface will be used on. 132 */ DeferrableSurface(@onNull Size size, int format)133 public DeferrableSurface(@NonNull Size size, int format) { 134 mPrescribedSize = size; 135 mPrescribedStreamFormat = format; 136 mTerminationFuture = CallbackToFutureAdapter.getFuture(completer -> { 137 synchronized (mLock) { 138 mTerminationCompleter = completer; 139 } 140 return "DeferrableSurface-termination(" + DeferrableSurface.this + ")"; 141 }); 142 143 mCloseFuture = CallbackToFutureAdapter.getFuture(completer -> { 144 synchronized (mLock) { 145 mCloseCompleter = completer; 146 } 147 return "DeferrableSurface-close(" + DeferrableSurface.this + ")"; 148 }); 149 150 if (Logger.isDebugEnabled(TAG)) { 151 printGlobalDebugCounts("Surface created", TOTAL_COUNT.incrementAndGet(), 152 USED_COUNT.get()); 153 154 String creationStackTrace = Log.getStackTraceString(new Exception()); 155 mTerminationFuture.addListener(() -> { 156 try { 157 mTerminationFuture.get(); 158 printGlobalDebugCounts("Surface terminated", TOTAL_COUNT.decrementAndGet(), 159 USED_COUNT.get()); 160 } catch (Exception e) { 161 Logger.e(TAG, "Unexpected surface termination for " + DeferrableSurface.this 162 + "\nStack Trace:\n" + creationStackTrace); 163 synchronized (mLock) { 164 throw new IllegalArgumentException(String.format( 165 "DeferrableSurface %s [closed: %b, use_count: %s] terminated with" 166 + " unexpected exception.", 167 DeferrableSurface.this, mClosed, mUseCount), e); 168 } 169 } 170 }, CameraXExecutors.directExecutor()); 171 } 172 } 173 printGlobalDebugCounts(@onNull String prefix, int totalCount, int useCount)174 private void printGlobalDebugCounts(@NonNull String prefix, int totalCount, int useCount) { 175 // If debug logging was not enabled at static initialization time but is now enabled, 176 // sUsedCount and sTotalCount may be inaccurate. 177 if (!DEBUG && Logger.isDebugEnabled(TAG)) { 178 Logger.d(TAG, 179 "DeferrableSurface usage statistics may be inaccurate since debug logging was" 180 + " not enabled at static initialization time. App restart may be " 181 + "required to enable accurate usage statistics."); 182 } 183 Logger.d(TAG, prefix + "[total_surfaces=" + totalCount + ", used_surfaces=" + useCount 184 + "](" + this + "}"); 185 } 186 187 /** 188 * Returns a {@link Surface} that is wrapped in a {@link ListenableFuture}. 189 * 190 * @return Will return a {@link ListenableFuture} with an exception if the DeferrableSurface 191 * is already closed. 192 */ getSurface()193 public final @NonNull ListenableFuture<Surface> getSurface() { 194 synchronized (mLock) { 195 if (mClosed) { 196 return Futures.immediateFailedFuture( 197 new SurfaceClosedException("DeferrableSurface already closed.", this)); 198 } 199 return provideSurface(); 200 } 201 } 202 203 /** 204 * Returns a {@link Surface} that is wrapped in a {@link ListenableFuture} when the 205 * DeferrableSurface has not yet been closed. 206 */ provideSurface()207 protected abstract @NonNull ListenableFuture<Surface> provideSurface(); 208 209 /** 210 * Returns a future which completes when the deferrable surface is terminated. 211 * 212 * <p>A deferrable surface is considered terminated once it has been closed by 213 * {@link #close()} and it is marked as no longer in use via {@link #decrementUseCount()}. 214 * 215 * <p>Once a deferrable surface has been terminated, it is safe to release all resources 216 * which may have been created for the surface. 217 * 218 * @return A future signalling the deferrable surface has terminated. Cancellation of this 219 * future is a no-op. 220 */ getTerminationFuture()221 public @NonNull ListenableFuture<Void> getTerminationFuture() { 222 return Futures.nonCancellationPropagating(mTerminationFuture); 223 } 224 225 /** 226 * Increments the use count of the surface. 227 * 228 * <p>If the surface has been closed and was not previously in use, this will fail and throw a 229 * {@link SurfaceClosedException} and the use count will not be incremented. 230 * 231 * @throws SurfaceClosedException if the surface has been closed. 232 */ incrementUseCount()233 public void incrementUseCount() throws SurfaceClosedException { 234 synchronized (mLock) { 235 if (mUseCount == 0 && mClosed) { 236 throw new SurfaceClosedException("Cannot begin use on a closed surface.", this); 237 } 238 mUseCount++; 239 240 if (Logger.isDebugEnabled(TAG)) { 241 if (mUseCount == 1) { 242 printGlobalDebugCounts("New surface in use", TOTAL_COUNT.get(), 243 USED_COUNT.incrementAndGet()); 244 } 245 Logger.d(TAG, "use count+1, useCount=" + mUseCount + " " + this); 246 } 247 } 248 } 249 250 /** 251 * Close the surface. 252 * 253 * <p>After closing, {@link #getSurface()} and {@link #incrementUseCount()} will return a 254 * {@link SurfaceClosedException}. 255 * 256 * <p>If the surface is not being used, then this will also complete the future returned by 257 * {@link #getTerminationFuture()}. If the surface is in use, then the future not be completed 258 * until {@link #decrementUseCount()} has bee called the appropriate number of times. 259 * 260 * <p>This method is idempotent. Subsequent calls after the first invocation will have no 261 * effect. 262 */ close()263 public void close() { 264 // If this gets set, then the surface will terminate 265 CallbackToFutureAdapter.Completer<Void> terminationCompleter = null; 266 synchronized (mLock) { 267 if (!mClosed) { 268 mClosed = true; 269 mCloseCompleter.set(null); 270 271 if (mUseCount == 0) { 272 terminationCompleter = mTerminationCompleter; 273 mTerminationCompleter = null; 274 } 275 276 if (Logger.isDebugEnabled(TAG)) { 277 Logger.d(TAG, 278 "surface closed, useCount=" + mUseCount + " closed=true " + this); 279 } 280 } 281 } 282 283 if (terminationCompleter != null) { 284 terminationCompleter.set(null); 285 } 286 } 287 288 /** 289 * Returns a future which completes when the deferrable surface is closed. 290 * 291 * <p>This is for propagating the closure to a upstream DeferrableSurface. For example, 292 * if StreamSharing is enabled, when Preview's DeferrableSurface is closed, the parent will 293 * listen to this future and close the connected {@link SurfaceEdge#getDeferrableSurface()}. 294 * 295 * @return A future signalling the deferrable surface is closed. Cancellation of this 296 * future is a no-op. 297 */ getCloseFuture()298 public @NonNull ListenableFuture<Void> getCloseFuture() { 299 return Futures.nonCancellationPropagating(mCloseFuture); 300 } 301 302 /** 303 * Decrements the use count. 304 * 305 * <p>If this causes the use count to go to zero and the surface has been closed, this will 306 * complete the future returned by {@link #getTerminationFuture()}. 307 */ decrementUseCount()308 public void decrementUseCount() { 309 // If this gets set, then the surface will terminate 310 CallbackToFutureAdapter.Completer<Void> terminationCompleter = null; 311 synchronized (mLock) { 312 if (mUseCount == 0) { 313 throw new IllegalStateException("Decrementing use count occurs more times than " 314 + "incrementing"); 315 } 316 317 mUseCount--; 318 if (mUseCount == 0 && mClosed) { 319 terminationCompleter = mTerminationCompleter; 320 mTerminationCompleter = null; 321 } 322 323 if (Logger.isDebugEnabled(TAG)) { 324 Logger.d(TAG, "use count-1, useCount=" + mUseCount + " closed=" + mClosed 325 + " " + this); 326 327 if (mUseCount == 0) { 328 printGlobalDebugCounts("Surface no longer in use", TOTAL_COUNT.get(), 329 USED_COUNT.decrementAndGet()); 330 } 331 } 332 } 333 334 if (terminationCompleter != null) { 335 terminationCompleter.set(null); 336 } 337 } 338 339 /** 340 * @return the {@link Size} of the surface 341 */ getPrescribedSize()342 public @NonNull Size getPrescribedSize() { 343 return mPrescribedSize; 344 } 345 346 /** 347 * @return the stream configuration format that the provided Surface will be used on. 348 */ getPrescribedStreamFormat()349 public int getPrescribedStreamFormat() { 350 return mPrescribedStreamFormat; 351 } 352 353 @VisibleForTesting getUseCount()354 public int getUseCount() { 355 synchronized (mLock) { 356 return mUseCount; 357 } 358 } 359 360 /** 361 * Checks if the {@link DeferrableSurface} is closed 362 */ isClosed()363 public boolean isClosed() { 364 synchronized (mLock) { 365 return mClosed; 366 } 367 } 368 369 /** 370 * Returns the {@link Class} that contains this {@link DeferrableSurface} to provide more 371 * context about it. 372 */ getContainerClass()373 public @Nullable Class<?> getContainerClass() { 374 return mContainerClass; 375 } 376 377 /** 378 * Set the {@link Class} that contains this {@link DeferrableSurface} to provide more 379 * context about it. 380 */ setContainerClass(@onNull Class<?> containerClass)381 public void setContainerClass(@NonNull Class<?> containerClass) { 382 mContainerClass = containerClass; 383 } 384 } 385