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