1 /*
2  * Copyright (C) 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 com.android.systemui.statusbar.phone;
18 
19 import static android.view.Display.DEFAULT_DISPLAY;
20 
21 import android.annotation.Nullable;
22 import android.content.res.Resources;
23 import android.graphics.Rect;
24 import android.os.Handler;
25 import android.os.IBinder;
26 import android.provider.Settings;
27 import android.view.CompositionSamplingListener;
28 import android.view.SurfaceControl;
29 import android.view.View;
30 import android.view.ViewRootImpl;
31 import android.view.ViewTreeObserver;
32 
33 import com.android.systemui.R;
34 
35 /**
36  * A helper class to sample regions on the screen and inspect its luminosity.
37  */
38 public class RegionSamplingHelper implements View.OnAttachStateChangeListener,
39         View.OnLayoutChangeListener {
40 
41     private final Handler mHandler = new Handler();
42     private final View mSampledView;
43 
44     private final CompositionSamplingListener mSamplingListener;
45     private final Runnable mUpdateSamplingListener = this::updateSamplingListener;
46 
47     /**
48      * The requested sampling bounds that we want to sample from
49      */
50     private final Rect mSamplingRequestBounds = new Rect();
51 
52     /**
53      * The sampling bounds that are currently registered.
54      */
55     private final Rect mRegisteredSamplingBounds = new Rect();
56     private final SamplingCallback mCallback;
57     private boolean mSamplingEnabled = false;
58     private boolean mSamplingListenerRegistered = false;
59 
60     private float mLastMedianLuma;
61     private float mCurrentMedianLuma;
62     private boolean mWaitingOnDraw;
63 
64     // Passing the threshold of this luminance value will make the button black otherwise white
65     private final float mLuminanceThreshold;
66     private final float mLuminanceChangeThreshold;
67     private boolean mFirstSamplingAfterStart;
68     private SurfaceControl mRegisteredStopLayer = null;
69     private ViewTreeObserver.OnDrawListener mUpdateOnDraw = new ViewTreeObserver.OnDrawListener() {
70         @Override
71         public void onDraw() {
72             // We need to post the remove runnable, since it's not allowed to remove in onDraw
73             mHandler.post(mRemoveDrawRunnable);
74             RegionSamplingHelper.this.onDraw();
75         }
76     };
77     private Runnable mRemoveDrawRunnable = new Runnable() {
78         @Override
79         public void run() {
80             mSampledView.getViewTreeObserver().removeOnDrawListener(mUpdateOnDraw);
81         }
82     };
83 
RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback)84     public RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback) {
85         mSamplingListener = new CompositionSamplingListener(
86                 sampledView.getContext().getMainExecutor()) {
87             @Override
88             public void onSampleCollected(float medianLuma) {
89                 if (mSamplingEnabled) {
90                     updateMediaLuma(medianLuma);
91                 }
92             }
93         };
94         mSampledView = sampledView;
95         mSampledView.addOnAttachStateChangeListener(this);
96         mSampledView.addOnLayoutChangeListener(this);
97 
98         final Resources res = sampledView.getResources();
99         mLuminanceThreshold = res.getFloat(R.dimen.navigation_luminance_threshold);
100         mLuminanceChangeThreshold = res.getFloat(R.dimen.navigation_luminance_change_threshold);
101         mCallback = samplingCallback;
102     }
103 
onDraw()104     private void onDraw() {
105         if (mWaitingOnDraw) {
106             mWaitingOnDraw = false;
107             updateSamplingListener();
108         }
109     }
110 
start(Rect initialSamplingBounds)111     void start(Rect initialSamplingBounds) {
112         if (!mCallback.isSamplingEnabled()) {
113             return;
114         }
115         if (initialSamplingBounds != null) {
116             mSamplingRequestBounds.set(initialSamplingBounds);
117         }
118         mSamplingEnabled = true;
119         // make sure we notify once
120         mLastMedianLuma = -1;
121         mFirstSamplingAfterStart = true;
122         updateSamplingListener();
123     }
124 
stop()125     void stop() {
126         mSamplingEnabled = false;
127         updateSamplingListener();
128     }
129 
stopAndDestroy()130     void stopAndDestroy() {
131         stop();
132         mSamplingListener.destroy();
133     }
134 
135     @Override
onViewAttachedToWindow(View view)136     public void onViewAttachedToWindow(View view) {
137         updateSamplingListener();
138     }
139 
140     @Override
onViewDetachedFromWindow(View view)141     public void onViewDetachedFromWindow(View view) {
142         stopAndDestroy();
143     }
144 
145     @Override
onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)146     public void onLayoutChange(View v, int left, int top, int right, int bottom,
147             int oldLeft, int oldTop, int oldRight, int oldBottom) {
148         updateSamplingRect();
149     }
150 
postUpdateSamplingListener()151     private void postUpdateSamplingListener() {
152         mHandler.removeCallbacks(mUpdateSamplingListener);
153         mHandler.post(mUpdateSamplingListener);
154     }
155 
updateSamplingListener()156     private void updateSamplingListener() {
157         boolean isSamplingEnabled = mSamplingEnabled && !mSamplingRequestBounds.isEmpty()
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 != null ? stopLayerControl.getHandle() : null,
188                         mSamplingRequestBounds);
189                 mRegisteredSamplingBounds.set(mSamplingRequestBounds);
190                 mRegisteredStopLayer = stopLayerControl;
191             }
192             mFirstSamplingAfterStart = false;
193         } else {
194             unregisterSamplingListener();
195         }
196     }
197 
unregisterSamplingListener()198     private void unregisterSamplingListener() {
199         if (mSamplingListenerRegistered) {
200             mSamplingListenerRegistered = false;
201             mRegisteredStopLayer = null;
202             mRegisteredSamplingBounds.setEmpty();
203             CompositionSamplingListener.unregister(mSamplingListener);
204         }
205     }
206 
updateMediaLuma(float medianLuma)207     private void updateMediaLuma(float medianLuma) {
208         mCurrentMedianLuma = medianLuma;
209 
210         // If the difference between the new luma and the current luma is larger than threshold
211         // then apply the current luma, this is to prevent small changes causing colors to flicker
212         if (Math.abs(mCurrentMedianLuma - mLastMedianLuma) > mLuminanceChangeThreshold) {
213             mCallback.onRegionDarknessChanged(medianLuma < mLuminanceThreshold /* isRegionDark */);
214             mLastMedianLuma = medianLuma;
215         }
216     }
217 
218     public void updateSamplingRect() {
219         Rect sampledRegion = mCallback.getSampledRegion(mSampledView);
220         if (!mSamplingRequestBounds.equals(sampledRegion)) {
221             mSamplingRequestBounds.set(sampledRegion);
222             updateSamplingListener();
223         }
224     }
225 
226     public interface SamplingCallback {
227         /**
228          * Called when the darkness of the sampled region changes
229          * @param isRegionDark true if the sampled luminance is below the luminance threshold
230          */
231         void onRegionDarknessChanged(boolean isRegionDark);
232 
233         /**
234          * Get the sampled region of interest from the sampled view
235          * @param sampledView The view that this helper is attached to for convenience
236          * @return the region to be sampled in sceen coordinates. Return {@code null} to avoid
237          * sampling in this frame
238          */
239         Rect getSampledRegion(View sampledView);
240 
241         /**
242          * @return if sampling should be enabled in the current configuration
243          */
244         default boolean isSamplingEnabled() {
245             return true;
246         }
247     }
248 }
249