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