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