1 /*
2  * Copyright 2023 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.view;
18 
19 import static androidx.camera.core.ImageCapture.FLASH_MODE_SCREEN;
20 import static androidx.camera.core.impl.utils.Threads.checkMainThread;
21 
22 import android.animation.Animator;
23 import android.animation.ValueAnimator;
24 import android.app.Activity;
25 import android.content.Context;
26 import android.graphics.Color;
27 import android.util.AttributeSet;
28 import android.view.View;
29 import android.view.Window;
30 import android.view.WindowManager;
31 
32 import androidx.annotation.RestrictTo;
33 import androidx.annotation.UiThread;
34 import androidx.camera.core.ImageCapture;
35 import androidx.camera.core.ImageCapture.ScreenFlash;
36 import androidx.camera.core.Logger;
37 import androidx.camera.view.internal.ScreenFlashUiInfo;
38 import androidx.fragment.app.Fragment;
39 
40 import org.jspecify.annotations.NonNull;
41 import org.jspecify.annotations.Nullable;
42 
43 /**
44  * Custom View that implements a basic UI for screen flash photo capture.
45  *
46  * <p> This class provides an {@link ScreenFlash} implementation with
47  * {@link #getScreenFlash()} for the
48  * {@link ImageCapture#setScreenFlash(ImageCapture.ScreenFlash)} API. If a
49  * {@link CameraController} is used for CameraX operations,{@link #setController(CameraController)}
50  * should be used to set the controller to this view. Normally, this view is kept fully
51  * transparent. It becomes fully visible for the duration of screen flash photo capture. The
52  * screen brightness is also maximized for that duration.
53  *
54  * <p> The default color of the view is {@link Color#WHITE}, but it can be changed with
55  * {@link View#setBackgroundColor(int)} API. The elevation of this view is always set to
56  * {@link Float#MAX_VALUE} so that it always appears on top in its view hierarchy during screen
57  * flash.
58  *
59  * <p> This view is also used internally in {@link PreviewView}, so may not be required if user
60  * is already using {@link PreviewView}. However, note that the internal instance of
61  * {@link PreviewView} has the same dimensions as {@link PreviewView}. So if the
62  * {@link PreviewView} does not encompass the full screen, users may want to use this view
63  * separately so that whole screen can be encompassed during screen flash operation.
64  *
65  * @see #getScreenFlash
66  * @see ImageCapture#FLASH_MODE_SCREEN
67  */
68 public final class ScreenFlashView extends View {
69     private static final String TAG = "ScreenFlashView";
70     private CameraController mCameraController;
71     private Window mScreenFlashWindow;
72     private ImageCapture.ScreenFlash mScreenFlash;
73 
74     /** The timeout in seconds for the visibility animation at {@link ScreenFlash#apply}. */
75     private static final long ANIMATION_DURATION_MILLIS = 1000;
76 
77     @UiThread
ScreenFlashView(@onNull Context context)78     public ScreenFlashView(@NonNull Context context) {
79         this(context, null);
80     }
81 
82     @UiThread
ScreenFlashView(@onNull Context context, @Nullable AttributeSet attrs)83     public ScreenFlashView(@NonNull Context context, @Nullable AttributeSet attrs) {
84         this(context, attrs, 0);
85     }
86 
87     @UiThread
ScreenFlashView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)88     public ScreenFlashView(@NonNull Context context, @Nullable AttributeSet attrs,
89             int defStyleAttr) {
90         this(context, attrs, defStyleAttr, 0);
91     }
92 
93     @UiThread
ScreenFlashView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)94     public ScreenFlashView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
95             int defStyleRes) {
96         super(context, attrs, defStyleAttr, defStyleRes);
97 
98         setBackgroundColor(Color.WHITE);
99         setAlpha(0f);
100         setElevation(Float.MAX_VALUE);
101     }
102 
103     /**
104      * Sets the {@link CameraController}.
105      *
106      * <p> Once set, the controller will use the {@code ScreenFlashView} for screen flash related UI
107      * operations.
108      *
109      * @throws IllegalStateException If {@link ImageCapture#FLASH_MODE_SCREEN} is set to the
110      *                               {@link CameraController}, but a non-null {@link Window}
111      *                               instance has not been set with {@link #setScreenFlashWindow}.
112      * @see CameraController
113      */
114     @UiThread
setController(@ullable CameraController cameraController)115     public void setController(@Nullable CameraController cameraController) {
116         checkMainThread();
117 
118         if (mCameraController != null && mCameraController != cameraController) {
119             // If already bound to a different controller, remove the ScreenFlash instance from the
120             // old controller.
121             setScreenFlashUiInfo(null);
122         }
123         mCameraController = cameraController;
124 
125         if (cameraController == null) {
126             return;
127         }
128 
129         if (cameraController.getImageCaptureFlashMode() == FLASH_MODE_SCREEN
130                 && mScreenFlashWindow == null) {
131             throw new IllegalStateException(
132                     "No window set despite setting FLASH_MODE_SCREEN in CameraController");
133         }
134 
135         setScreenFlashUiInfo(getScreenFlash());
136     }
137 
setScreenFlashUiInfo(ImageCapture.ScreenFlash control)138     private void setScreenFlashUiInfo(ImageCapture.ScreenFlash control) {
139         if (mCameraController == null) {
140             Logger.d(TAG, "setScreenFlashUiInfo: mCameraController is null!");
141             return;
142         }
143         mCameraController.setScreenFlashUiInfo(new ScreenFlashUiInfo(
144                 ScreenFlashUiInfo.ProviderType.SCREEN_FLASH_VIEW, control));
145     }
146 
147     /**
148      * Sets a {@link Window} instance for subsequent photo capture requests with
149      * {@link ImageCapture} use case when {@link ImageCapture#FLASH_MODE_SCREEN} is set.
150      *
151      * <p>The calling of this API will take effect for {@code ImageCapture#FLASH_MODE_SCREEN} only
152      * and the {@code Window} will be ignored for other flash modes. During screen flash photo
153      * capture, the window is used for the purpose of changing screen brightness.
154      *
155      * <p> If the implementation provided by the user is no longer valid (e.g. due to any
156      * {@link android.app.Activity} or {@link android.view.View} reference used in the
157      * implementation becoming invalid), user needs to re-set a new valid window or clear the
158      * previous one with {@code setScreenFlashWindow(null)}, whichever appropriate.
159      *
160      * <p>For most app scenarios, a {@code Window} instance can be obtained from
161      * {@link Activity#getWindow()}. In case of a fragment, {@link Fragment#getActivity()} can
162      * first be used to get the activity instance.
163      *
164      * @param screenFlashWindow A {@link Window} instance that is used to change the brightness
165      *                          during screen flash photo capture.
166      */
167     @UiThread
setScreenFlashWindow(@ullable Window screenFlashWindow)168     public void setScreenFlashWindow(@Nullable Window screenFlashWindow) {
169         checkMainThread();
170         updateScreenFlash(screenFlashWindow);
171         mScreenFlashWindow = screenFlashWindow;
172         setScreenFlashUiInfo(getScreenFlash());
173     }
174 
175     /** Update {@link #mScreenFlash} if required. */
updateScreenFlash(Window window)176     private void updateScreenFlash(Window window) {
177         Logger.d(TAG, "updateScreenFlash: is new window null = " + (window == null)
178                 + ",  is new window same as previous = " + (window == mScreenFlashWindow));
179 
180         if (mScreenFlashWindow != window) {
181             mScreenFlash = window == null ? null : new ScreenFlash() {
182                 private float mPreviousBrightness;
183                 private ValueAnimator mAnimator;
184 
185                 @Override
186                 public void apply(long expirationTimeMillis,
187                         ImageCapture.@NonNull ScreenFlashListener screenFlashListener) {
188                     Logger.d(TAG, "ScreenFlash#apply");
189 
190                     mPreviousBrightness = getBrightness();
191                     setBrightness(1.0f);
192 
193                     if (mAnimator != null) {
194                         mAnimator.cancel();
195                     }
196                     mAnimator = animateToFullOpacity(screenFlashListener::onCompleted);
197                 }
198 
199                 @Override
200                 public void clear() {
201                     Logger.d(TAG, "ScreenFlash#clear");
202 
203                     if (mAnimator != null) {
204                         mAnimator.cancel();
205                         mAnimator = null;
206                     }
207 
208                     setAlpha(0f);
209 
210                     // Restore screen brightness
211                     setBrightness(mPreviousBrightness);
212                 }
213             };
214         }
215     }
216 
animateToFullOpacity(@ullable Runnable onAnimationEnd)217     private ValueAnimator animateToFullOpacity(@Nullable Runnable onAnimationEnd) {
218         Logger.d(TAG, "animateToFullOpacity");
219 
220         ValueAnimator animator = ValueAnimator.ofFloat(0F, 1F);
221 
222         // TODO: b/355168952 - Allow users to overwrite the animation duration.
223         animator.setDuration(getVisibilityRampUpAnimationDurationMillis());
224 
225         animator.addUpdateListener(animation -> {
226             Logger.d(TAG, "animateToFullOpacity: value = " + (float) animation.getAnimatedValue());
227             setAlpha((float) animation.getAnimatedValue());
228         });
229 
230         animator.addListener(new Animator.AnimatorListener() {
231             @Override
232             public void onAnimationStart(@NonNull Animator animation) {
233 
234             }
235 
236             @Override
237             public void onAnimationEnd(@NonNull Animator animation) {
238                 Logger.d(TAG, "ScreenFlash#apply: onAnimationEnd");
239                 if (onAnimationEnd != null) {
240                     onAnimationEnd.run();
241                 }
242             }
243 
244             @Override
245             public void onAnimationCancel(@NonNull Animator animation) {
246 
247             }
248 
249             @Override
250             public void onAnimationRepeat(@NonNull Animator animation) {
251 
252             }
253         });
254 
255         animator.start();
256 
257         return animator;
258     }
259 
getBrightness()260     private float getBrightness() {
261         if (mScreenFlashWindow == null) {
262             Logger.e(TAG, "setBrightness: mScreenFlashWindow is null!");
263             return Float.NaN;
264         }
265 
266         WindowManager.LayoutParams layoutParam = mScreenFlashWindow.getAttributes();
267         return layoutParam.screenBrightness;
268     }
269 
setBrightness(float value)270     private void setBrightness(float value) {
271         if (mScreenFlashWindow == null) {
272             Logger.e(TAG, "setBrightness: mScreenFlashWindow is null!");
273             return;
274         }
275 
276         if (Float.isNaN(value)) {
277             Logger.e(TAG, "setBrightness: value is NaN!");
278             return;
279         }
280 
281         WindowManager.LayoutParams layoutParam = mScreenFlashWindow.getAttributes();
282         layoutParam.screenBrightness = value;
283         mScreenFlashWindow.setAttributes(layoutParam);
284         Logger.d(TAG, "Brightness set to " + layoutParam.screenBrightness);
285     }
286 
287     /**
288      * Returns an {@link ScreenFlash} implementation based on the {@link Window} instance
289      * set via {@link #setScreenFlashWindow(Window)}.
290      *
291      * <p> When {@link ScreenFlash#apply(long, ImageCapture.ScreenFlashListener)} is invoked,
292      * this view becomes fully visible gradually with an animation and screen brightness is
293      * maximized using the provided {@code Window}. Since brightness change of the display happens
294      * asynchronously and may take some time to be completed, the animation to ramp up visibility
295      * may require a duration of sufficient delay (decided internally) before
296      * {@link ImageCapture.ScreenFlashListener#onCompleted()} is invoked.
297      *
298      * <p> The default color of the overlay view is {@link Color#WHITE}. To change
299      * the color, use {@link #setBackgroundColor(int)}.
300      *
301      * <p> When {@link ScreenFlash#clear()} is invoked, the view
302      * becomes transparent and screen brightness is restored.
303      *
304      * <p> The {@code Window} instance parameter can usually be provided from the activity using
305      * the {@link PreviewView}, see {@link Activity#getWindow()} for details. If a null {@code
306      * Window} is set or none set at all, a null value will be returned by this method.
307      *
308      * @return A simple {@link ScreenFlash} implementation, or null value if a non-null
309      * {@code Window} instance hasn't been set.
310      */
311     @UiThread
getScreenFlash()312     public @Nullable ScreenFlash getScreenFlash() {
313         return mScreenFlash;
314     }
315 
316     /**
317      * Returns the duration of the visibility ramp-up animation.
318      *
319      * <p> This is currently used in {@link ScreenFlash#apply}.
320      *
321      * @see #getScreenFlash()
322      */
323     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
getVisibilityRampUpAnimationDurationMillis()324     public long getVisibilityRampUpAnimationDurationMillis() {
325         return ANIMATION_DURATION_MILLIS;
326     }
327 }
328