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