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