/* * Copyright 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.display; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ParceledListSlice; import android.database.ContentObserver; import android.graphics.PixelFormat; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.hardware.display.AmbientBrightnessDayStats; import android.hardware.display.BrightnessChangeEvent; import android.hardware.display.ColorDisplayManager; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerInternal; import android.hardware.display.DisplayedContentSample; import android.hardware.display.DisplayedContentSamplingAttributes; import android.net.Uri; import android.os.BatteryManager; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.util.AtomicFile; import android.util.Slog; import android.util.Xml; import android.view.Display; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.RingBuffer; import com.android.server.LocalServices; import libcore.io.IoUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Date; import java.util.Deque; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; /** * Class that tracks recent brightness settings changes and stores * associated information such as light sensor readings. */ public class BrightnessTracker { static final String TAG = "BrightnessTracker"; static final boolean DEBUG = false; private static final String EVENTS_FILE = "brightness_events.xml"; private static final String AMBIENT_BRIGHTNESS_STATS_FILE = "ambient_brightness_stats.xml"; private static final int MAX_EVENTS = 100; // Discard events when reading or writing that are older than this. private static final long MAX_EVENT_AGE = TimeUnit.DAYS.toMillis(30); // Time over which we keep lux sensor readings. private static final long LUX_EVENT_HORIZON = TimeUnit.SECONDS.toNanos(10); private static final String TAG_EVENTS = "events"; private static final String TAG_EVENT = "event"; private static final String ATTR_NITS = "nits"; private static final String ATTR_TIMESTAMP = "timestamp"; private static final String ATTR_PACKAGE_NAME = "packageName"; private static final String ATTR_USER = "user"; private static final String ATTR_LUX = "lux"; private static final String ATTR_LUX_TIMESTAMPS = "luxTimestamps"; private static final String ATTR_BATTERY_LEVEL = "batteryLevel"; private static final String ATTR_NIGHT_MODE = "nightMode"; private static final String ATTR_COLOR_TEMPERATURE = "colorTemperature"; private static final String ATTR_LAST_NITS = "lastNits"; private static final String ATTR_DEFAULT_CONFIG = "defaultConfig"; private static final String ATTR_POWER_SAVE = "powerSaveFactor"; private static final String ATTR_USER_POINT = "userPoint"; private static final String ATTR_COLOR_SAMPLE_DURATION = "colorSampleDuration"; private static final String ATTR_COLOR_VALUE_BUCKETS = "colorValueBuckets"; private static final int MSG_BACKGROUND_START = 0; private static final int MSG_BRIGHTNESS_CHANGED = 1; private static final int MSG_STOP_SENSOR_LISTENER = 2; private static final int MSG_START_SENSOR_LISTENER = 3; private static final SimpleDateFormat FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); private static final long COLOR_SAMPLE_DURATION = TimeUnit.SECONDS.toSeconds(10); // Sample chanel 2 of HSV which is the Value component. private static final int COLOR_SAMPLE_COMPONENT_MASK = 0x1 << 2; // Lock held while accessing mEvents, is held while writing events to flash. private final Object mEventsLock = new Object(); @GuardedBy("mEventsLock") private RingBuffer mEvents = new RingBuffer<>(BrightnessChangeEvent.class, MAX_EVENTS); @GuardedBy("mEventsLock") private boolean mEventsDirty; private volatile boolean mWriteBrightnessTrackerStateScheduled; private AmbientBrightnessStatsTracker mAmbientBrightnessStatsTracker; private final UserManager mUserManager; private final Context mContext; private final ContentResolver mContentResolver; private final Handler mBgHandler; // These members should only be accessed on the mBgHandler thread. private BroadcastReceiver mBroadcastReceiver; private SensorListener mSensorListener; private SettingsObserver mSettingsObserver; private DisplayListener mDisplayListener; private boolean mSensorRegistered; private boolean mColorSamplingEnabled; private int mNoFramesToSample; private float mFrameRate; // End of block of members that should only be accessed on the mBgHandler thread. private @UserIdInt int mCurrentUserId = UserHandle.USER_NULL; // Lock held while collecting data related to brightness changes. private final Object mDataCollectionLock = new Object(); @GuardedBy("mDataCollectionLock") private Deque mLastSensorReadings = new ArrayDeque<>(); @GuardedBy("mDataCollectionLock") private float mLastBatteryLevel = Float.NaN; @GuardedBy("mDataCollectionLock") private float mLastBrightness = -1; @GuardedBy("mDataCollectionLock") private boolean mStarted; private final Injector mInjector; public BrightnessTracker(Context context, @Nullable Injector injector) { // Note this will be called very early in boot, other system // services may not be present. mContext = context; mContentResolver = context.getContentResolver(); if (injector != null) { mInjector = injector; } else { mInjector = new Injector(); } mBgHandler = new TrackerHandler(mInjector.getBackgroundHandler().getLooper()); mUserManager = mContext.getSystemService(UserManager.class); } /** * Start listening for brightness slider events * * @param initialBrightness the initial screen brightness */ public void start(float initialBrightness) { if (DEBUG) { Slog.d(TAG, "Start"); } mCurrentUserId = ActivityManager.getCurrentUser(); mBgHandler.obtainMessage(MSG_BACKGROUND_START, (Float) initialBrightness).sendToTarget(); } private void backgroundStart(float initialBrightness) { readEvents(); readAmbientBrightnessStats(); mSensorListener = new SensorListener(); mSettingsObserver = new SettingsObserver(mBgHandler); mInjector.registerBrightnessModeObserver(mContentResolver, mSettingsObserver); startSensorListener(); final IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_SHUTDOWN); intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED); intentFilter.addAction(Intent.ACTION_SCREEN_ON); intentFilter.addAction(Intent.ACTION_SCREEN_OFF); mBroadcastReceiver = new Receiver(); mInjector.registerReceiver(mContext, mBroadcastReceiver, intentFilter); mInjector.scheduleIdleJob(mContext); synchronized (mDataCollectionLock) { mLastBrightness = initialBrightness; mStarted = true; } enableColorSampling(); } /** Stop listening for events */ @VisibleForTesting void stop() { if (DEBUG) { Slog.d(TAG, "Stop"); } mBgHandler.removeMessages(MSG_BACKGROUND_START); stopSensorListener(); mInjector.unregisterSensorListener(mContext, mSensorListener); mInjector.unregisterBrightnessModeObserver(mContext, mSettingsObserver); mInjector.unregisterReceiver(mContext, mBroadcastReceiver); mInjector.cancelIdleJob(mContext); synchronized (mDataCollectionLock) { mStarted = false; } disableColorSampling(); } public void onSwitchUser(@UserIdInt int newUserId) { if (DEBUG) { Slog.d(TAG, "Used id updated from " + mCurrentUserId + " to " + newUserId); } mCurrentUserId = newUserId; } /** * @param userId userId to fetch data for. * @param includePackage if false we will null out BrightnessChangeEvent.packageName * @return List of recent {@link BrightnessChangeEvent}s */ public ParceledListSlice getEvents(int userId, boolean includePackage) { BrightnessChangeEvent[] events; synchronized (mEventsLock) { events = mEvents.toArray(); } int[] profiles = mInjector.getProfileIds(mUserManager, userId); Map toRedact = new HashMap<>(); for (int i = 0; i < profiles.length; ++i) { int profileId = profiles[i]; // Include slider interactions when a managed profile app is in the // foreground but always redact the package name. boolean redact = (!includePackage) || profileId != userId; toRedact.put(profiles[i], redact); } ArrayList out = new ArrayList<>(events.length); for (int i = 0; i < events.length; ++i) { Boolean redact = toRedact.get(events[i].userId); if (redact != null) { if (!redact) { out.add(events[i]); } else { BrightnessChangeEvent event = new BrightnessChangeEvent((events[i]), /* redactPackage */ true); out.add(event); } } } return new ParceledListSlice<>(out); } public void persistBrightnessTrackerState() { scheduleWriteBrightnessTrackerState(); } /** * Notify the BrightnessTracker that the user has changed the brightness of the display. */ public void notifyBrightnessChanged(float brightness, boolean userInitiated, float powerBrightnessFactor, boolean isUserSetBrightness, boolean isDefaultBrightnessConfig) { if (DEBUG) { Slog.d(TAG, String.format("notifyBrightnessChanged(brightness=%f, userInitiated=%b)", brightness, userInitiated)); } Message m = mBgHandler.obtainMessage(MSG_BRIGHTNESS_CHANGED, userInitiated ? 1 : 0, 0 /*unused*/, new BrightnessChangeValues(brightness, powerBrightnessFactor, isUserSetBrightness, isDefaultBrightnessConfig, mInjector.currentTimeMillis())); m.sendToTarget(); } private void handleBrightnessChanged(float brightness, boolean userInitiated, float powerBrightnessFactor, boolean isUserSetBrightness, boolean isDefaultBrightnessConfig, long timestamp) { BrightnessChangeEvent.Builder builder; synchronized (mDataCollectionLock) { if (!mStarted) { // Not currently gathering brightness change information return; } float previousBrightness = mLastBrightness; mLastBrightness = brightness; if (!userInitiated) { // We want to record what current brightness is so that we know what the user // changed it from, but if it wasn't user initiated then we don't want to record it // as a BrightnessChangeEvent. return; } builder = new BrightnessChangeEvent.Builder(); builder.setBrightness(brightness); builder.setTimeStamp(timestamp); builder.setPowerBrightnessFactor(powerBrightnessFactor); builder.setUserBrightnessPoint(isUserSetBrightness); builder.setIsDefaultBrightnessConfig(isDefaultBrightnessConfig); final int readingCount = mLastSensorReadings.size(); if (readingCount == 0) { // No sensor data so ignore this. return; } float[] luxValues = new float[readingCount]; long[] luxTimestamps = new long[readingCount]; int pos = 0; // Convert sensor timestamp in elapsed time nanos to current time millis. long currentTimeMillis = mInjector.currentTimeMillis(); long elapsedTimeNanos = mInjector.elapsedRealtimeNanos(); for (LightData reading : mLastSensorReadings) { luxValues[pos] = reading.lux; luxTimestamps[pos] = currentTimeMillis - TimeUnit.NANOSECONDS.toMillis(elapsedTimeNanos - reading.timestamp); ++pos; } builder.setLuxValues(luxValues); builder.setLuxTimestamps(luxTimestamps); builder.setBatteryLevel(mLastBatteryLevel); builder.setLastBrightness(previousBrightness); } try { final ActivityManager.StackInfo focusedStack = mInjector.getFocusedStack(); if (focusedStack != null && focusedStack.topActivity != null) { builder.setUserId(focusedStack.userId); builder.setPackageName(focusedStack.topActivity.getPackageName()); } else { // Ignore the event because we can't determine user / package. if (DEBUG) { Slog.d(TAG, "Ignoring event due to null focusedStack."); } return; } } catch (RemoteException e) { // Really shouldn't be possible. return; } builder.setNightMode(mInjector.isNightDisplayActivated(mContext)); builder.setColorTemperature(mInjector.getNightDisplayColorTemperature(mContext)); if (mColorSamplingEnabled) { DisplayedContentSample sample = mInjector.sampleColor(mNoFramesToSample); if (sample != null && sample.getSampleComponent( DisplayedContentSample.ColorComponent.CHANNEL2) != null) { float numMillis = (sample.getNumFrames() / mFrameRate) * 1000.0f; builder.setColorValues( sample.getSampleComponent(DisplayedContentSample.ColorComponent.CHANNEL2), Math.round(numMillis)); } } BrightnessChangeEvent event = builder.build(); if (DEBUG) { Slog.d(TAG, "Event " + event.brightness + " " + event.packageName); } synchronized (mEventsLock) { mEventsDirty = true; mEvents.append(event); } } private void startSensorListener() { if (!mSensorRegistered && mInjector.isInteractive(mContext) && mInjector.isBrightnessModeAutomatic(mContentResolver)) { mAmbientBrightnessStatsTracker.start(); mSensorRegistered = true; mInjector.registerSensorListener(mContext, mSensorListener, mInjector.getBackgroundHandler()); } } private void stopSensorListener() { if (mSensorRegistered) { mAmbientBrightnessStatsTracker.stop(); mInjector.unregisterSensorListener(mContext, mSensorListener); mSensorRegistered = false; } } private void scheduleWriteBrightnessTrackerState() { if (!mWriteBrightnessTrackerStateScheduled) { mBgHandler.post(() -> { mWriteBrightnessTrackerStateScheduled = false; writeEvents(); writeAmbientBrightnessStats(); }); mWriteBrightnessTrackerStateScheduled = true; } } private void writeEvents() { synchronized (mEventsLock) { if (!mEventsDirty) { // Nothing to write return; } final AtomicFile writeTo = mInjector.getFile(EVENTS_FILE); if (writeTo == null) { return; } if (mEvents.isEmpty()) { if (writeTo.exists()) { writeTo.delete(); } mEventsDirty = false; } else { FileOutputStream output = null; try { output = writeTo.startWrite(); writeEventsLocked(output); writeTo.finishWrite(output); mEventsDirty = false; } catch (IOException e) { writeTo.failWrite(output); Slog.e(TAG, "Failed to write change mEvents.", e); } } } } private void writeAmbientBrightnessStats() { final AtomicFile writeTo = mInjector.getFile(AMBIENT_BRIGHTNESS_STATS_FILE); if (writeTo == null) { return; } FileOutputStream output = null; try { output = writeTo.startWrite(); mAmbientBrightnessStatsTracker.writeStats(output); writeTo.finishWrite(output); } catch (IOException e) { writeTo.failWrite(output); Slog.e(TAG, "Failed to write ambient brightness stats.", e); } } private void readEvents() { synchronized (mEventsLock) { // Read might prune events so mark as dirty. mEventsDirty = true; mEvents.clear(); final AtomicFile readFrom = mInjector.getFile(EVENTS_FILE); if (readFrom != null && readFrom.exists()) { FileInputStream input = null; try { input = readFrom.openRead(); readEventsLocked(input); } catch (IOException e) { readFrom.delete(); Slog.e(TAG, "Failed to read change mEvents.", e); } finally { IoUtils.closeQuietly(input); } } } } private void readAmbientBrightnessStats() { mAmbientBrightnessStatsTracker = new AmbientBrightnessStatsTracker(mUserManager, null); final AtomicFile readFrom = mInjector.getFile(AMBIENT_BRIGHTNESS_STATS_FILE); if (readFrom != null && readFrom.exists()) { FileInputStream input = null; try { input = readFrom.openRead(); mAmbientBrightnessStatsTracker.readStats(input); } catch (IOException e) { readFrom.delete(); Slog.e(TAG, "Failed to read ambient brightness stats.", e); } finally { IoUtils.closeQuietly(input); } } } @VisibleForTesting @GuardedBy("mEventsLock") void writeEventsLocked(OutputStream stream) throws IOException { XmlSerializer out = new FastXmlSerializer(); out.setOutput(stream, StandardCharsets.UTF_8.name()); out.startDocument(null, true); out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); out.startTag(null, TAG_EVENTS); BrightnessChangeEvent[] toWrite = mEvents.toArray(); // Clear events, code below will add back the ones that are still within the time window. mEvents.clear(); if (DEBUG) { Slog.d(TAG, "Writing events " + toWrite.length); } final long timeCutOff = mInjector.currentTimeMillis() - MAX_EVENT_AGE; for (int i = 0; i < toWrite.length; ++i) { int userSerialNo = mInjector.getUserSerialNumber(mUserManager, toWrite[i].userId); if (userSerialNo != -1 && toWrite[i].timeStamp > timeCutOff) { mEvents.append(toWrite[i]); out.startTag(null, TAG_EVENT); out.attribute(null, ATTR_NITS, Float.toString(toWrite[i].brightness)); out.attribute(null, ATTR_TIMESTAMP, Long.toString(toWrite[i].timeStamp)); out.attribute(null, ATTR_PACKAGE_NAME, toWrite[i].packageName); out.attribute(null, ATTR_USER, Integer.toString(userSerialNo)); out.attribute(null, ATTR_BATTERY_LEVEL, Float.toString(toWrite[i].batteryLevel)); out.attribute(null, ATTR_NIGHT_MODE, Boolean.toString(toWrite[i].nightMode)); out.attribute(null, ATTR_COLOR_TEMPERATURE, Integer.toString( toWrite[i].colorTemperature)); out.attribute(null, ATTR_LAST_NITS, Float.toString(toWrite[i].lastBrightness)); out.attribute(null, ATTR_DEFAULT_CONFIG, Boolean.toString(toWrite[i].isDefaultBrightnessConfig)); out.attribute(null, ATTR_POWER_SAVE, Float.toString(toWrite[i].powerBrightnessFactor)); out.attribute(null, ATTR_USER_POINT, Boolean.toString(toWrite[i].isUserSetBrightness)); StringBuilder luxValues = new StringBuilder(); StringBuilder luxTimestamps = new StringBuilder(); for (int j = 0; j < toWrite[i].luxValues.length; ++j) { if (j > 0) { luxValues.append(','); luxTimestamps.append(','); } luxValues.append(Float.toString(toWrite[i].luxValues[j])); luxTimestamps.append(Long.toString(toWrite[i].luxTimestamps[j])); } out.attribute(null, ATTR_LUX, luxValues.toString()); out.attribute(null, ATTR_LUX_TIMESTAMPS, luxTimestamps.toString()); if (toWrite[i].colorValueBuckets != null && toWrite[i].colorValueBuckets.length > 0) { out.attribute(null, ATTR_COLOR_SAMPLE_DURATION, Long.toString(toWrite[i].colorSampleDuration)); StringBuilder buckets = new StringBuilder(); for (int j = 0; j < toWrite[i].colorValueBuckets.length; ++j) { if (j > 0) { buckets.append(','); } buckets.append(Long.toString(toWrite[i].colorValueBuckets[j])); } out.attribute(null, ATTR_COLOR_VALUE_BUCKETS, buckets.toString()); } out.endTag(null, TAG_EVENT); } } out.endTag(null, TAG_EVENTS); out.endDocument(); stream.flush(); } @VisibleForTesting @GuardedBy("mEventsLock") void readEventsLocked(InputStream stream) throws IOException { try { XmlPullParser parser = Xml.newPullParser(); parser.setInput(stream, StandardCharsets.UTF_8.name()); int type; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { } String tag = parser.getName(); if (!TAG_EVENTS.equals(tag)) { throw new XmlPullParserException( "Events not found in brightness tracker file " + tag); } final long timeCutOff = mInjector.currentTimeMillis() - MAX_EVENT_AGE; parser.next(); int outerDepth = parser.getDepth(); while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { continue; } tag = parser.getName(); if (TAG_EVENT.equals(tag)) { BrightnessChangeEvent.Builder builder = new BrightnessChangeEvent.Builder(); String brightness = parser.getAttributeValue(null, ATTR_NITS); builder.setBrightness(Float.parseFloat(brightness)); String timestamp = parser.getAttributeValue(null, ATTR_TIMESTAMP); builder.setTimeStamp(Long.parseLong(timestamp)); builder.setPackageName(parser.getAttributeValue(null, ATTR_PACKAGE_NAME)); String user = parser.getAttributeValue(null, ATTR_USER); builder.setUserId(mInjector.getUserId(mUserManager, Integer.parseInt(user))); String batteryLevel = parser.getAttributeValue(null, ATTR_BATTERY_LEVEL); builder.setBatteryLevel(Float.parseFloat(batteryLevel)); String nightMode = parser.getAttributeValue(null, ATTR_NIGHT_MODE); builder.setNightMode(Boolean.parseBoolean(nightMode)); String colorTemperature = parser.getAttributeValue(null, ATTR_COLOR_TEMPERATURE); builder.setColorTemperature(Integer.parseInt(colorTemperature)); String lastBrightness = parser.getAttributeValue(null, ATTR_LAST_NITS); builder.setLastBrightness(Float.parseFloat(lastBrightness)); String luxValue = parser.getAttributeValue(null, ATTR_LUX); String luxTimestamp = parser.getAttributeValue(null, ATTR_LUX_TIMESTAMPS); String[] luxValuesStrings = luxValue.split(","); String[] luxTimestampsStrings = luxTimestamp.split(","); if (luxValuesStrings.length != luxTimestampsStrings.length) { continue; } float[] luxValues = new float[luxValuesStrings.length]; long[] luxTimestamps = new long[luxValuesStrings.length]; for (int i = 0; i < luxValues.length; ++i) { luxValues[i] = Float.parseFloat(luxValuesStrings[i]); luxTimestamps[i] = Long.parseLong(luxTimestampsStrings[i]); } builder.setLuxValues(luxValues); builder.setLuxTimestamps(luxTimestamps); String defaultConfig = parser.getAttributeValue(null, ATTR_DEFAULT_CONFIG); if (defaultConfig != null) { builder.setIsDefaultBrightnessConfig(Boolean.parseBoolean(defaultConfig)); } String powerSave = parser.getAttributeValue(null, ATTR_POWER_SAVE); if (powerSave != null) { builder.setPowerBrightnessFactor(Float.parseFloat(powerSave)); } else { builder.setPowerBrightnessFactor(1.0f); } String userPoint = parser.getAttributeValue(null, ATTR_USER_POINT); if (userPoint != null) { builder.setUserBrightnessPoint(Boolean.parseBoolean(userPoint)); } String colorSampleDurationString = parser.getAttributeValue(null, ATTR_COLOR_SAMPLE_DURATION); String colorValueBucketsString = parser.getAttributeValue(null, ATTR_COLOR_VALUE_BUCKETS); if (colorSampleDurationString != null && colorValueBucketsString != null) { long colorSampleDuration = Long.parseLong(colorSampleDurationString); String[] buckets = colorValueBucketsString.split(","); long[] bucketValues = new long[buckets.length]; for (int i = 0; i < bucketValues.length; ++i) { bucketValues[i] = Long.parseLong(buckets[i]); } builder.setColorValues(bucketValues, colorSampleDuration); } BrightnessChangeEvent event = builder.build(); if (DEBUG) { Slog.i(TAG, "Read event " + event.brightness + " " + event.packageName); } if (event.userId != -1 && event.timeStamp > timeCutOff && event.luxValues.length > 0) { mEvents.append(event); } } } } catch (NullPointerException | NumberFormatException | XmlPullParserException | IOException e) { // Failed to parse something, just start with an empty event log. mEvents = new RingBuffer<>(BrightnessChangeEvent.class, MAX_EVENTS); Slog.e(TAG, "Failed to parse brightness event", e); // Re-throw so we will delete the bad file. throw new IOException("failed to parse file", e); } } public void dump(final PrintWriter pw) { pw.println("BrightnessTracker state:"); synchronized (mDataCollectionLock) { pw.println(" mStarted=" + mStarted); pw.println(" mLastBatteryLevel=" + mLastBatteryLevel); pw.println(" mLastBrightness=" + mLastBrightness); pw.println(" mLastSensorReadings.size=" + mLastSensorReadings.size()); if (!mLastSensorReadings.isEmpty()) { pw.println(" mLastSensorReadings time span " + mLastSensorReadings.peekFirst().timestamp + "->" + mLastSensorReadings.peekLast().timestamp); } } synchronized (mEventsLock) { pw.println(" mEventsDirty=" + mEventsDirty); pw.println(" mEvents.size=" + mEvents.size()); BrightnessChangeEvent[] events = mEvents.toArray(); for (int i = 0; i < events.length; ++i) { pw.print(" " + FORMAT.format(new Date(events[i].timeStamp))); pw.print(", userId=" + events[i].userId); pw.print(", " + events[i].lastBrightness + "->" + events[i].brightness); pw.print(", isUserSetBrightness=" + events[i].isUserSetBrightness); pw.print(", powerBrightnessFactor=" + events[i].powerBrightnessFactor); pw.print(", isDefaultBrightnessConfig=" + events[i].isDefaultBrightnessConfig); pw.print(" {"); for (int j = 0; j < events[i].luxValues.length; ++j){ if (j != 0) { pw.print(", "); } pw.print("(" + events[i].luxValues[j] + "," + events[i].luxTimestamps[j] + ")"); } pw.println("}"); } } pw.println(" mWriteBrightnessTrackerStateScheduled=" + mWriteBrightnessTrackerStateScheduled); mBgHandler.runWithScissors(() -> dumpLocal(pw), 1000); if (mAmbientBrightnessStatsTracker != null) { pw.println(); mAmbientBrightnessStatsTracker.dump(pw); } } private void dumpLocal(PrintWriter pw) { pw.println(" mSensorRegistered=" + mSensorRegistered); pw.println(" mColorSamplingEnabled=" + mColorSamplingEnabled); pw.println(" mNoFramesToSample=" + mNoFramesToSample); pw.println(" mFrameRate=" + mFrameRate); } private void enableColorSampling() { if (!mInjector.isBrightnessModeAutomatic(mContentResolver) || !mInjector.isInteractive(mContext) || mColorSamplingEnabled) { return; } mFrameRate = mInjector.getFrameRate(mContext); if (mFrameRate <= 0) { Slog.wtf(TAG, "Default display has a zero or negative framerate."); return; } mNoFramesToSample = (int) (mFrameRate * COLOR_SAMPLE_DURATION); DisplayedContentSamplingAttributes attributes = mInjector.getSamplingAttributes(); if (DEBUG && attributes != null) { Slog.d(TAG, "Color sampling" + " mask=0x" + Integer.toHexString(attributes.getComponentMask()) + " dataSpace=0x" + Integer.toHexString(attributes.getDataspace()) + " pixelFormat=0x" + Integer.toHexString(attributes.getPixelFormat())); } // Do we support sampling the Value component of HSV if (attributes != null && attributes.getPixelFormat() == PixelFormat.HSV_888 && (attributes.getComponentMask() & COLOR_SAMPLE_COMPONENT_MASK) != 0) { mColorSamplingEnabled = mInjector.enableColorSampling(/* enable= */true, mNoFramesToSample); if (DEBUG) { Slog.i(TAG, "turning on color sampling for " + mNoFramesToSample + " frames, success=" + mColorSamplingEnabled); } } if (mColorSamplingEnabled && mDisplayListener == null) { mDisplayListener = new DisplayListener(); mInjector.registerDisplayListener(mContext, mDisplayListener, mBgHandler); } } private void disableColorSampling() { if (!mColorSamplingEnabled) { return; } mInjector.enableColorSampling(/* enable= */ false, /* noFrames= */ 0); mColorSamplingEnabled = false; if (mDisplayListener != null) { mInjector.unRegisterDisplayListener(mContext, mDisplayListener); mDisplayListener = null; } if (DEBUG) { Slog.i(TAG, "turning off color sampling"); } } private void updateColorSampling() { if (!mColorSamplingEnabled) { return; } float frameRate = mInjector.getFrameRate(mContext); if (frameRate != mFrameRate) { disableColorSampling(); enableColorSampling(); } } public ParceledListSlice getAmbientBrightnessStats(int userId) { if (mAmbientBrightnessStatsTracker != null) { ArrayList stats = mAmbientBrightnessStatsTracker.getUserStats(userId); if (stats != null) { return new ParceledListSlice<>(stats); } } return ParceledListSlice.emptyList(); } // Not allowed to keep the SensorEvent so used to copy the data we care about. private static class LightData { public float lux; // Time in elapsedRealtimeNanos public long timestamp; } private void recordSensorEvent(SensorEvent event) { long horizon = mInjector.elapsedRealtimeNanos() - LUX_EVENT_HORIZON; synchronized (mDataCollectionLock) { if (DEBUG) { Slog.v(TAG, "Sensor event " + event); } if (!mLastSensorReadings.isEmpty() && event.timestamp < mLastSensorReadings.getLast().timestamp) { // Ignore event that came out of order. return; } LightData data = null; while (!mLastSensorReadings.isEmpty() && mLastSensorReadings.getFirst().timestamp < horizon) { // Remove data that has fallen out of the window. data = mLastSensorReadings.removeFirst(); } // We put back the last one we removed so we know how long // the first sensor reading was valid for. if (data != null) { mLastSensorReadings.addFirst(data); } data = new LightData(); data.timestamp = event.timestamp; data.lux = event.values[0]; mLastSensorReadings.addLast(data); } } private void recordAmbientBrightnessStats(SensorEvent event) { mAmbientBrightnessStatsTracker.add(mCurrentUserId, event.values[0]); } private void batteryLevelChanged(int level, int scale) { synchronized (mDataCollectionLock) { mLastBatteryLevel = (float) level / (float) scale; } } private final class SensorListener implements SensorEventListener { @Override public void onSensorChanged(SensorEvent event) { recordSensorEvent(event); recordAmbientBrightnessStats(event); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } } private final class DisplayListener implements DisplayManager.DisplayListener { @Override public void onDisplayAdded(int displayId) { // Ignore } @Override public void onDisplayRemoved(int displayId) { // Ignore } @Override public void onDisplayChanged(int displayId) { if (displayId == Display.DEFAULT_DISPLAY) { updateColorSampling(); } } } private final class SettingsObserver extends ContentObserver { public SettingsObserver(Handler handler) { super(handler); } @Override public void onChange(boolean selfChange, Uri uri) { if (DEBUG) { Slog.v(TAG, "settings change " + uri); } if (mInjector.isBrightnessModeAutomatic(mContentResolver)) { mBgHandler.obtainMessage(MSG_START_SENSOR_LISTENER).sendToTarget(); } else { mBgHandler.obtainMessage(MSG_STOP_SENSOR_LISTENER).sendToTarget(); } } } private final class Receiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (DEBUG) { Slog.d(TAG, "Received " + intent.getAction()); } String action = intent.getAction(); if (Intent.ACTION_SHUTDOWN.equals(action)) { stop(); scheduleWriteBrightnessTrackerState(); } else if (Intent.ACTION_BATTERY_CHANGED.equals(action)) { int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0); if (level != -1 && scale != 0) { batteryLevelChanged(level, scale); } } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { mBgHandler.obtainMessage(MSG_STOP_SENSOR_LISTENER).sendToTarget(); } else if (Intent.ACTION_SCREEN_ON.equals(action)) { mBgHandler.obtainMessage(MSG_START_SENSOR_LISTENER).sendToTarget(); } } } private final class TrackerHandler extends Handler { public TrackerHandler(Looper looper) { super(looper, null, true /*async*/); } public void handleMessage(Message msg) { switch (msg.what) { case MSG_BACKGROUND_START: backgroundStart((float)msg.obj /*initial brightness*/); break; case MSG_BRIGHTNESS_CHANGED: BrightnessChangeValues values = (BrightnessChangeValues) msg.obj; boolean userInitiatedChange = (msg.arg1 == 1); handleBrightnessChanged(values.brightness, userInitiatedChange, values.powerBrightnessFactor, values.isUserSetBrightness, values.isDefaultBrightnessConfig, values.timestamp); break; case MSG_START_SENSOR_LISTENER: startSensorListener(); enableColorSampling(); break; case MSG_STOP_SENSOR_LISTENER: stopSensorListener(); disableColorSampling(); break; } } } private static class BrightnessChangeValues { final float brightness; final float powerBrightnessFactor; final boolean isUserSetBrightness; final boolean isDefaultBrightnessConfig; final long timestamp; BrightnessChangeValues(float brightness, float powerBrightnessFactor, boolean isUserSetBrightness, boolean isDefaultBrightnessConfig, long timestamp) { this.brightness = brightness; this.powerBrightnessFactor = powerBrightnessFactor; this.isUserSetBrightness = isUserSetBrightness; this.isDefaultBrightnessConfig = isDefaultBrightnessConfig; this.timestamp = timestamp; } } @VisibleForTesting static class Injector { public void registerSensorListener(Context context, SensorEventListener sensorListener, Handler handler) { SensorManager sensorManager = context.getSystemService(SensorManager.class); Sensor lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); sensorManager.registerListener(sensorListener, lightSensor, SensorManager.SENSOR_DELAY_NORMAL, handler); } public void unregisterSensorListener(Context context, SensorEventListener sensorListener) { SensorManager sensorManager = context.getSystemService(SensorManager.class); sensorManager.unregisterListener(sensorListener); } public void registerBrightnessModeObserver(ContentResolver resolver, ContentObserver settingsObserver) { resolver.registerContentObserver(Settings.System.getUriFor( Settings.System.SCREEN_BRIGHTNESS_MODE), false, settingsObserver, UserHandle.USER_ALL); } public void unregisterBrightnessModeObserver(Context context, ContentObserver settingsObserver) { context.getContentResolver().unregisterContentObserver(settingsObserver); } public void registerReceiver(Context context, BroadcastReceiver receiver, IntentFilter filter) { context.registerReceiver(receiver, filter); } public void unregisterReceiver(Context context, BroadcastReceiver receiver) { context.unregisterReceiver(receiver); } public Handler getBackgroundHandler() { return BackgroundThread.getHandler(); } public boolean isBrightnessModeAutomatic(ContentResolver resolver) { return Settings.System.getIntForUser(resolver, Settings.System.SCREEN_BRIGHTNESS_MODE, Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT) == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC; } public int getSecureIntForUser(ContentResolver resolver, String setting, int defaultValue, int userId) { return Settings.Secure.getIntForUser(resolver, setting, defaultValue, userId); } public AtomicFile getFile(String filename) { return new AtomicFile(new File(Environment.getDataSystemDeDirectory(), filename)); } public long currentTimeMillis() { return System.currentTimeMillis(); } public long elapsedRealtimeNanos() { return SystemClock.elapsedRealtimeNanos(); } public int getUserSerialNumber(UserManager userManager, int userId) { return userManager.getUserSerialNumber(userId); } public int getUserId(UserManager userManager, int userSerialNumber) { return userManager.getUserHandle(userSerialNumber); } public int[] getProfileIds(UserManager userManager, int userId) { if (userManager != null) { return userManager.getProfileIds(userId, false); } else { return new int[]{userId}; } } public ActivityManager.StackInfo getFocusedStack() throws RemoteException { return ActivityTaskManager.getService().getFocusedStackInfo(); } public void scheduleIdleJob(Context context) { BrightnessIdleJob.scheduleJob(context); } public void cancelIdleJob(Context context) { BrightnessIdleJob.cancelJob(context); } public boolean isInteractive(Context context) { return context.getSystemService(PowerManager.class).isInteractive(); } public int getNightDisplayColorTemperature(Context context) { return context.getSystemService(ColorDisplayManager.class) .getNightDisplayColorTemperature(); } public boolean isNightDisplayActivated(Context context) { return context.getSystemService(ColorDisplayManager.class).isNightDisplayActivated(); } public DisplayedContentSample sampleColor(int noFramesToSample) { final DisplayManagerInternal displayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); return displayManagerInternal.getDisplayedContentSample( Display.DEFAULT_DISPLAY, noFramesToSample, 0); } public float getFrameRate(Context context) { final DisplayManager displayManager = context.getSystemService(DisplayManager.class); Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); return display.getRefreshRate(); } public DisplayedContentSamplingAttributes getSamplingAttributes() { final DisplayManagerInternal displayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); return displayManagerInternal.getDisplayedContentSamplingAttributes( Display.DEFAULT_DISPLAY); } public boolean enableColorSampling(boolean enable, int noFrames) { final DisplayManagerInternal displayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); return displayManagerInternal.setDisplayedContentSamplingEnabled( Display.DEFAULT_DISPLAY, enable, COLOR_SAMPLE_COMPONENT_MASK, noFrames); } public void registerDisplayListener(Context context, DisplayManager.DisplayListener listener, Handler handler) { final DisplayManager displayManager = context.getSystemService(DisplayManager.class); displayManager.registerDisplayListener(listener, handler); } public void unRegisterDisplayListener(Context context, DisplayManager.DisplayListener listener) { final DisplayManager displayManager = context.getSystemService(DisplayManager.class); displayManager.unregisterDisplayListener(listener); } } }