• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
130     @Override
onViewAttachedToWindow(View view)131     public void onViewAttachedToWindow(View view) {
132         updateSamplingListener();
133     }
134 
135     @Override
onViewDetachedFromWindow(View view)136     public void onViewDetachedFromWindow(View view) {
137         // isAttachedToWindow is only changed after this call to the listeners, so let's post it
138         // instead
139         postUpdateSamplingListener();
140     }
141 
142     @Override
onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)143     public void onLayoutChange(View v, int left, int top, int right, int bottom,
144             int oldLeft, int oldTop, int oldRight, int oldBottom) {
145         updateSamplingRect();
146     }
147 
postUpdateSamplingListener()148     private void postUpdateSamplingListener() {
149         mHandler.removeCallbacks(mUpdateSamplingListener);
150         mHandler.post(mUpdateSamplingListener);
151     }
152 
updateSamplingListener()153     private void updateSamplingListener() {
154         boolean isSamplingEnabled = mSamplingEnabled && !mSamplingRequestBounds.isEmpty()
155                 && (mSampledView.isAttachedToWindow() || mFirstSamplingAfterStart);
156         if (isSamplingEnabled) {
157             ViewRootImpl viewRootImpl = mSampledView.getViewRootImpl();
158             SurfaceControl stopLayerControl = null;
159             if (viewRootImpl != null) {
160                  stopLayerControl = viewRootImpl.getSurfaceControl();
161             }
162             if (stopLayerControl == null || !stopLayerControl.isValid()) {
163                 if (!mWaitingOnDraw) {
164                     mWaitingOnDraw = true;
165                     // The view might be attached but we haven't drawn yet, so wait until the
166                     // next draw to update the listener again with the stop layer, such that our
167                     // own drawing doesn't affect the sampling.
168                     if (mHandler.hasCallbacks(mRemoveDrawRunnable)) {
169                         mHandler.removeCallbacks(mRemoveDrawRunnable);
170                     } else {
171                         mSampledView.getViewTreeObserver().addOnDrawListener(mUpdateOnDraw);
172                     }
173                 }
174                 // If there's no valid surface, let's just sample without a stop layer, so we
175                 // don't have to delay
176                 stopLayerControl = null;
177             }
178             if (!mSamplingRequestBounds.equals(mRegisteredSamplingBounds)
179                     || mRegisteredStopLayer != stopLayerControl) {
180                 // We only want to reregister if something actually changed
181                 unregisterSamplingListener();
182                 mSamplingListenerRegistered = true;
183                 CompositionSamplingListener.register(mSamplingListener, DEFAULT_DISPLAY,
184                         stopLayerControl != null ? stopLayerControl.getHandle() : null,
185                         mSamplingRequestBounds);
186                 mRegisteredSamplingBounds.set(mSamplingRequestBounds);
187                 mRegisteredStopLayer = stopLayerControl;
188             }
189             mFirstSamplingAfterStart = false;
190         } else {
191             unregisterSamplingListener();
192         }
193     }
194 
unregisterSamplingListener()195     private void unregisterSamplingListener() {
196         if (mSamplingListenerRegistered) {
197             mSamplingListenerRegistered = false;
198             mRegisteredStopLayer = null;
199             mRegisteredSamplingBounds.setEmpty();
200             CompositionSamplingListener.unregister(mSamplingListener);
201         }
202     }
203 
updateMediaLuma(float medianLuma)204     private void updateMediaLuma(float medianLuma) {
205         mCurrentMedianLuma = medianLuma;
206 
207         // If the difference between the new luma and the current luma is larger than threshold
208         // then apply the current luma, this is to prevent small changes causing colors to flicker
209         if (Math.abs(mCurrentMedianLuma - mLastMedianLuma) > mLuminanceChangeThreshold) {
210             mCallback.onRegionDarknessChanged(medianLuma < mLuminanceThreshold /* isRegionDark */);
211             mLastMedianLuma = medianLuma;
212         }
213     }
214 
215     public void updateSamplingRect() {
216         Rect sampledRegion = mCallback.getSampledRegion(mSampledView);
217         if (!mSamplingRequestBounds.equals(sampledRegion)) {
218             mSamplingRequestBounds.set(sampledRegion);
219             updateSamplingListener();
220         }
221     }
222 
223     public interface SamplingCallback {
224         /**
225          * Called when the darkness of the sampled region changes
226          * @param isRegionDark true if the sampled luminance is below the luminance threshold
227          */
228         void onRegionDarknessChanged(boolean isRegionDark);
229 
230         /**
231          * Get the sampled region of interest from the sampled view
232          * @param sampledView The view that this helper is attached to for convenience
233          * @return the region to be sampled in sceen coordinates. Return {@code null} to avoid
234          * sampling in this frame
235          */
236         Rect getSampledRegion(View sampledView);
237 
238         /**
239          * @return if sampling should be enabled in the current configuration
240          */
241         default boolean isSamplingEnabled() {
242             return true;
243         }
244     }
245 }
246