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