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.systemui.shared.navigationbar;
18 
19 import static android.view.Display.DEFAULT_DISPLAY;
20 
21 import android.annotation.TargetApi;
22 import android.graphics.Rect;
23 import android.os.Build;
24 import android.os.Handler;
25 import android.view.CompositionSamplingListener;
26 import android.view.SurfaceControl;
27 import android.view.View;
28 import android.view.ViewRootImpl;
29 import android.view.ViewTreeObserver;
30 
31 import java.io.PrintWriter;
32 import java.util.concurrent.Executor;
33 
34 /**
35  * A helper class to sample regions on the screen and inspect its luminosity.
36  */
37 @TargetApi(Build.VERSION_CODES.Q)
38 public class RegionSamplingHelper implements View.OnAttachStateChangeListener,
39         View.OnLayoutChangeListener {
40 
41     // Luminance threshold to determine black/white contrast for the navigation affordances.
42     // Passing the threshold of this luminance value will make the button black otherwise white
43     private static final float NAVIGATION_LUMINANCE_THRESHOLD = 0.5f;
44     // Luminance change threshold that allows applying new value if difference was exceeded
45     private static final float NAVIGATION_LUMINANCE_CHANGE_THRESHOLD = 0.05f;
46 
47     private final Handler mHandler = new Handler();
48     private final View mSampledView;
49 
50     private final CompositionSamplingListener mSamplingListener;
51 
52     /**
53      * The requested sampling bounds that we want to sample from
54      */
55     private final Rect mSamplingRequestBounds = new Rect();
56 
57     /**
58      * The sampling bounds that are currently registered.
59      */
60     private final Rect mRegisteredSamplingBounds = new Rect();
61     private final SamplingCallback mCallback;
62     private final Executor mBackgroundExecutor;
63     private boolean mSamplingEnabled = false;
64     private boolean mSamplingListenerRegistered = false;
65 
66     private float mLastMedianLuma;
67     private float mCurrentMedianLuma;
68     private boolean mWaitingOnDraw;
69     private boolean mIsDestroyed;
70 
71     private boolean mFirstSamplingAfterStart;
72     private boolean mWindowVisible;
73     private boolean mWindowHasBlurs;
74     private SurfaceControl mRegisteredStopLayer = null;
75     // A copy of mRegisteredStopLayer where we own the life cycle and can access from a bg thread.
76     private SurfaceControl mWrappedStopLayer = null;
77     private ViewTreeObserver.OnDrawListener mUpdateOnDraw = new ViewTreeObserver.OnDrawListener() {
78         @Override
79         public void onDraw() {
80             // We need to post the remove runnable, since it's not allowed to remove in onDraw
81             mHandler.post(mRemoveDrawRunnable);
82             RegionSamplingHelper.this.onDraw();
83         }
84     };
85     private Runnable mRemoveDrawRunnable = new Runnable() {
86         @Override
87         public void run() {
88             mSampledView.getViewTreeObserver().removeOnDrawListener(mUpdateOnDraw);
89         }
90     };
91 
RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback, Executor backgroundExecutor)92     public RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback,
93             Executor backgroundExecutor) {
94         mBackgroundExecutor = backgroundExecutor;
95         mSamplingListener = new CompositionSamplingListener(
96                 sampledView.getContext().getMainExecutor()) {
97             @Override
98             public void onSampleCollected(float medianLuma) {
99                 if (mSamplingEnabled) {
100                     updateMediaLuma(medianLuma);
101                 }
102             }
103         };
104         mSampledView = sampledView;
105         mSampledView.addOnAttachStateChangeListener(this);
106         mSampledView.addOnLayoutChangeListener(this);
107 
108         mCallback = samplingCallback;
109     }
110 
onDraw()111     private void onDraw() {
112         if (mWaitingOnDraw) {
113             mWaitingOnDraw = false;
114             updateSamplingListener();
115         }
116     }
117 
start(Rect initialSamplingBounds)118     public void start(Rect initialSamplingBounds) {
119         if (!mCallback.isSamplingEnabled()) {
120             return;
121         }
122         if (initialSamplingBounds != null) {
123             mSamplingRequestBounds.set(initialSamplingBounds);
124         }
125         mSamplingEnabled = true;
126         // make sure we notify once
127         mLastMedianLuma = -1;
128         mFirstSamplingAfterStart = true;
129         updateSamplingListener();
130     }
131 
stop()132     public void stop() {
133         mSamplingEnabled = false;
134         updateSamplingListener();
135     }
136 
stopAndDestroy()137     public void stopAndDestroy() {
138         stop();
139         mSamplingListener.destroy();
140         mIsDestroyed = true;
141     }
142 
143     @Override
onViewAttachedToWindow(View view)144     public void onViewAttachedToWindow(View view) {
145         updateSamplingListener();
146     }
147 
148     @Override
onViewDetachedFromWindow(View view)149     public void onViewDetachedFromWindow(View view) {
150         stopAndDestroy();
151     }
152 
153     @Override
onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)154     public void onLayoutChange(View v, int left, int top, int right, int bottom,
155             int oldLeft, int oldTop, int oldRight, int oldBottom) {
156         updateSamplingRect();
157     }
158 
updateSamplingListener()159     private void updateSamplingListener() {
160         boolean isSamplingEnabled = mSamplingEnabled
161                 && !mSamplingRequestBounds.isEmpty()
162                 && mWindowVisible
163                 && !mWindowHasBlurs
164                 && (mSampledView.isAttachedToWindow() || mFirstSamplingAfterStart);
165         if (isSamplingEnabled) {
166             ViewRootImpl viewRootImpl = mSampledView.getViewRootImpl();
167             SurfaceControl stopLayerControl = null;
168             if (viewRootImpl != null) {
169                  stopLayerControl = viewRootImpl.getSurfaceControl();
170             }
171             if (stopLayerControl == null || !stopLayerControl.isValid()) {
172                 if (!mWaitingOnDraw) {
173                     mWaitingOnDraw = true;
174                     // The view might be attached but we haven't drawn yet, so wait until the
175                     // next draw to update the listener again with the stop layer, such that our
176                     // own drawing doesn't affect the sampling.
177                     if (mHandler.hasCallbacks(mRemoveDrawRunnable)) {
178                         mHandler.removeCallbacks(mRemoveDrawRunnable);
179                     } else {
180                         mSampledView.getViewTreeObserver().addOnDrawListener(mUpdateOnDraw);
181                     }
182                 }
183                 // If there's no valid surface, let's just sample without a stop layer, so we
184                 // don't have to delay
185                 stopLayerControl = null;
186             }
187             if (!mSamplingRequestBounds.equals(mRegisteredSamplingBounds)
188                     || mRegisteredStopLayer != stopLayerControl) {
189                 // We only want to re-register if something actually changed
190                 unregisterSamplingListener();
191                 mSamplingListenerRegistered = true;
192                 SurfaceControl wrappedStopLayer = stopLayerControl == null
193                         ? null : new SurfaceControl(stopLayerControl, "regionSampling");
194                 mBackgroundExecutor.execute(() -> {
195                     if (wrappedStopLayer != null && !wrappedStopLayer.isValid()) {
196                         return;
197                     }
198                     CompositionSamplingListener.register(mSamplingListener, DEFAULT_DISPLAY,
199                             wrappedStopLayer, mSamplingRequestBounds);
200                 });
201                 mRegisteredSamplingBounds.set(mSamplingRequestBounds);
202                 mRegisteredStopLayer = stopLayerControl;
203                 mWrappedStopLayer = wrappedStopLayer;
204             }
205             mFirstSamplingAfterStart = false;
206         } else {
207             unregisterSamplingListener();
208         }
209     }
210 
unregisterSamplingListener()211     private void unregisterSamplingListener() {
212         if (mSamplingListenerRegistered) {
213             mSamplingListenerRegistered = false;
214             SurfaceControl wrappedStopLayer = mWrappedStopLayer;
215             mRegisteredStopLayer = null;
216             mRegisteredSamplingBounds.setEmpty();
217             mBackgroundExecutor.execute(() -> {
218                 CompositionSamplingListener.unregister(mSamplingListener);
219                 if (wrappedStopLayer != null && wrappedStopLayer.isValid()) {
220                     wrappedStopLayer.release();
221                 }
222             });
223         }
224     }
225 
updateMediaLuma(float medianLuma)226     private void updateMediaLuma(float medianLuma) {
227         mCurrentMedianLuma = medianLuma;
228 
229         // If the difference between the new luma and the current luma is larger than threshold
230         // then apply the current luma, this is to prevent small changes causing colors to flicker
231         if (Math.abs(mCurrentMedianLuma - mLastMedianLuma)
232                 > NAVIGATION_LUMINANCE_CHANGE_THRESHOLD) {
233             mCallback.onRegionDarknessChanged(
234                     medianLuma < NAVIGATION_LUMINANCE_THRESHOLD /* isRegionDark */);
235             mLastMedianLuma = medianLuma;
236         }
237     }
238 
239     public void updateSamplingRect() {
240         Rect sampledRegion = mCallback.getSampledRegion(mSampledView);
241         if (!mSamplingRequestBounds.equals(sampledRegion)) {
242             mSamplingRequestBounds.set(sampledRegion);
243             updateSamplingListener();
244         }
245     }
246 
247     public void setWindowVisible(boolean visible) {
248         mWindowVisible = visible;
249         updateSamplingListener();
250     }
251 
252     /**
253      * If we're blurring the shade window.
254      */
255     public void setWindowHasBlurs(boolean hasBlurs) {
256         mWindowHasBlurs = hasBlurs;
257         updateSamplingListener();
258     }
259 
260     public void dump(PrintWriter pw) {
261         pw.println("RegionSamplingHelper:");
262         pw.println("  sampleView isAttached: " + mSampledView.isAttachedToWindow());
263         pw.println("  sampleView isScValid: " + (mSampledView.isAttachedToWindow()
264                 ? mSampledView.getViewRootImpl().getSurfaceControl().isValid()
265                 : "notAttached"));
266         pw.println("  mSamplingEnabled: " + mSamplingEnabled);
267         pw.println("  mSamplingListenerRegistered: " + mSamplingListenerRegistered);
268         pw.println("  mSamplingRequestBounds: " + mSamplingRequestBounds);
269         pw.println("  mRegisteredSamplingBounds: " + mRegisteredSamplingBounds);
270         pw.println("  mLastMedianLuma: " + mLastMedianLuma);
271         pw.println("  mCurrentMedianLuma: " + mCurrentMedianLuma);
272         pw.println("  mWindowVisible: " + mWindowVisible);
273         pw.println("  mWindowHasBlurs: " + mWindowHasBlurs);
274         pw.println("  mWaitingOnDraw: " + mWaitingOnDraw);
275         pw.println("  mRegisteredStopLayer: " + mRegisteredStopLayer);
276         pw.println("  mWrappedStopLayer: " + mWrappedStopLayer);
277         pw.println("  mIsDestroyed: " + mIsDestroyed);
278     }
279 
280     public interface SamplingCallback {
281         /**
282          * Called when the darkness of the sampled region changes
283          * @param isRegionDark true if the sampled luminance is below the luminance threshold
284          */
285         void onRegionDarknessChanged(boolean isRegionDark);
286 
287         /**
288          * Get the sampled region of interest from the sampled view
289          * @param sampledView The view that this helper is attached to for convenience
290          * @return the region to be sampled in sceen coordinates. Return {@code null} to avoid
291          * sampling in this frame
292          */
293         Rect getSampledRegion(View sampledView);
294 
295         /**
296          * @return if sampling should be enabled in the current configuration
297          */
298         default boolean isSamplingEnabled() {
299             return true;
300         }
301     }
302 }
303