/* * Copyright (C) 2012 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 com.android.internal.util.FastXmlSerializer; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import android.annotation.Nullable; import android.graphics.Point; import android.hardware.display.BrightnessConfiguration; import android.hardware.display.WifiDisplay; import android.util.AtomicFile; import android.util.Slog; import android.util.SparseArray; import android.util.Pair; import android.util.SparseLongArray; import android.util.TimeUtils; import android.util.Xml; import android.view.Display; import com.android.internal.annotations.VisibleForTesting; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; 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.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import libcore.io.IoUtils; /** * Manages persistent state recorded by the display manager service as an XML file. * Caller must acquire lock on the data store before accessing it. * * File format: * * <display-manager-state> * <remembered-wifi-displays> * <wifi-display deviceAddress="00:00:00:00:00:00" deviceName="XXXX" deviceAlias="YYYY" /> * <remembered-wifi-displays> * <display-states> * <display unique-id="XXXXXXX"> * <color-mode>0</color-mode> * </display> * </display-states> * <stable-device-values> * <stable-display-height>1920</stable-display-height> * <stable-display-width>1080</stable-display-width> * </stable-device-values> * <brightness-configurations> * <brightness-configuration user-serial="0" package-name="com.example" timestamp="1234"> * <brightness-curve description="some text"> * <brightness-point lux="0" nits="13.25"/> * <brightness-point lux="20" nits="35.94"/> * </brightness-curve> * </brightness-configuration> * </brightness-configurations> * </display-manager-state> * * * TODO: refactor this to extract common code shared with the input manager's data store */ final class PersistentDataStore { static final String TAG = "DisplayManager"; private static final String TAG_DISPLAY_MANAGER_STATE = "display-manager-state"; private static final String TAG_REMEMBERED_WIFI_DISPLAYS = "remembered-wifi-displays"; private static final String TAG_WIFI_DISPLAY = "wifi-display"; private static final String ATTR_DEVICE_ADDRESS = "deviceAddress"; private static final String ATTR_DEVICE_NAME = "deviceName"; private static final String ATTR_DEVICE_ALIAS = "deviceAlias"; private static final String TAG_DISPLAY_STATES = "display-states"; private static final String TAG_DISPLAY = "display"; private static final String TAG_COLOR_MODE = "color-mode"; private static final String ATTR_UNIQUE_ID = "unique-id"; private static final String TAG_STABLE_DEVICE_VALUES = "stable-device-values"; private static final String TAG_STABLE_DISPLAY_HEIGHT = "stable-display-height"; private static final String TAG_STABLE_DISPLAY_WIDTH = "stable-display-width"; private static final String TAG_BRIGHTNESS_CONFIGURATIONS = "brightness-configurations"; private static final String TAG_BRIGHTNESS_CONFIGURATION = "brightness-configuration"; private static final String TAG_BRIGHTNESS_CURVE = "brightness-curve"; private static final String TAG_BRIGHTNESS_POINT = "brightness-point"; private static final String ATTR_USER_SERIAL = "user-serial"; private static final String ATTR_PACKAGE_NAME = "package-name"; private static final String ATTR_TIME_STAMP = "timestamp"; private static final String ATTR_LUX = "lux"; private static final String ATTR_NITS = "nits"; private static final String ATTR_DESCRIPTION = "description"; // Remembered Wifi display devices. private ArrayList mRememberedWifiDisplays = new ArrayList(); // Display state by unique id. private final HashMap mDisplayStates = new HashMap(); // Display values which should be stable across the device's lifetime. private final StableDeviceValues mStableDeviceValues = new StableDeviceValues(); // Brightness configuration by user private BrightnessConfigurations mBrightnessConfigurations = new BrightnessConfigurations(); // True if the data has been loaded. private boolean mLoaded; // True if there are changes to be saved. private boolean mDirty; // The interface for methods which should be replaced by the test harness. private Injector mInjector; public PersistentDataStore() { this(new Injector()); } @VisibleForTesting PersistentDataStore(Injector injector) { mInjector = injector; } public void saveIfNeeded() { if (mDirty) { save(); mDirty = false; } } public WifiDisplay getRememberedWifiDisplay(String deviceAddress) { loadIfNeeded(); int index = findRememberedWifiDisplay(deviceAddress); if (index >= 0) { return mRememberedWifiDisplays.get(index); } return null; } public WifiDisplay[] getRememberedWifiDisplays() { loadIfNeeded(); return mRememberedWifiDisplays.toArray(new WifiDisplay[mRememberedWifiDisplays.size()]); } public WifiDisplay applyWifiDisplayAlias(WifiDisplay display) { if (display != null) { loadIfNeeded(); String alias = null; int index = findRememberedWifiDisplay(display.getDeviceAddress()); if (index >= 0) { alias = mRememberedWifiDisplays.get(index).getDeviceAlias(); } if (!Objects.equals(display.getDeviceAlias(), alias)) { return new WifiDisplay(display.getDeviceAddress(), display.getDeviceName(), alias, display.isAvailable(), display.canConnect(), display.isRemembered()); } } return display; } public WifiDisplay[] applyWifiDisplayAliases(WifiDisplay[] displays) { WifiDisplay[] results = displays; if (results != null) { int count = displays.length; for (int i = 0; i < count; i++) { WifiDisplay result = applyWifiDisplayAlias(displays[i]); if (result != displays[i]) { if (results == displays) { results = new WifiDisplay[count]; System.arraycopy(displays, 0, results, 0, count); } results[i] = result; } } } return results; } public boolean rememberWifiDisplay(WifiDisplay display) { loadIfNeeded(); int index = findRememberedWifiDisplay(display.getDeviceAddress()); if (index >= 0) { WifiDisplay other = mRememberedWifiDisplays.get(index); if (other.equals(display)) { return false; // already remembered without change } mRememberedWifiDisplays.set(index, display); } else { mRememberedWifiDisplays.add(display); } setDirty(); return true; } public boolean forgetWifiDisplay(String deviceAddress) { loadIfNeeded(); int index = findRememberedWifiDisplay(deviceAddress); if (index >= 0) { mRememberedWifiDisplays.remove(index); setDirty(); return true; } return false; } private int findRememberedWifiDisplay(String deviceAddress) { int count = mRememberedWifiDisplays.size(); for (int i = 0; i < count; i++) { if (mRememberedWifiDisplays.get(i).getDeviceAddress().equals(deviceAddress)) { return i; } } return -1; } public int getColorMode(DisplayDevice device) { if (!device.hasStableUniqueId()) { return Display.COLOR_MODE_INVALID; } DisplayState state = getDisplayState(device.getUniqueId(), false); if (state == null) { return Display.COLOR_MODE_INVALID; } return state.getColorMode(); } public boolean setColorMode(DisplayDevice device, int colorMode) { if (!device.hasStableUniqueId()) { return false; } DisplayState state = getDisplayState(device.getUniqueId(), true); if (state.setColorMode(colorMode)) { setDirty(); return true; } return false; } public Point getStableDisplaySize() { loadIfNeeded(); return mStableDeviceValues.getDisplaySize(); } public void setStableDisplaySize(Point size) { loadIfNeeded(); if (mStableDeviceValues.setDisplaySize(size)) { setDirty(); } } public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userSerial, @Nullable String packageName) { loadIfNeeded(); if (mBrightnessConfigurations.setBrightnessConfigurationForUser(c, userSerial, packageName)) { setDirty(); } } public BrightnessConfiguration getBrightnessConfiguration(int userSerial) { loadIfNeeded(); return mBrightnessConfigurations.getBrightnessConfiguration(userSerial); } private DisplayState getDisplayState(String uniqueId, boolean createIfAbsent) { loadIfNeeded(); DisplayState state = mDisplayStates.get(uniqueId); if (state == null && createIfAbsent) { state = new DisplayState(); mDisplayStates.put(uniqueId, state); setDirty(); } return state; } public void loadIfNeeded() { if (!mLoaded) { load(); mLoaded = true; } } private void setDirty() { mDirty = true; } private void clearState() { mRememberedWifiDisplays.clear(); } private void load() { clearState(); final InputStream is; try { is = mInjector.openRead(); } catch (FileNotFoundException ex) { return; } XmlPullParser parser; try { parser = Xml.newPullParser(); parser.setInput(new BufferedInputStream(is), StandardCharsets.UTF_8.name()); loadFromXml(parser); } catch (IOException ex) { Slog.w(TAG, "Failed to load display manager persistent store data.", ex); clearState(); } catch (XmlPullParserException ex) { Slog.w(TAG, "Failed to load display manager persistent store data.", ex); clearState(); } finally { IoUtils.closeQuietly(is); } } private void save() { final OutputStream os; try { os = mInjector.startWrite(); boolean success = false; try { XmlSerializer serializer = new FastXmlSerializer(); serializer.setOutput(new BufferedOutputStream(os), StandardCharsets.UTF_8.name()); saveToXml(serializer); serializer.flush(); success = true; } finally { mInjector.finishWrite(os, success); } } catch (IOException ex) { Slog.w(TAG, "Failed to save display manager persistent store data.", ex); } } private void loadFromXml(XmlPullParser parser) throws IOException, XmlPullParserException { XmlUtils.beginDocument(parser, TAG_DISPLAY_MANAGER_STATE); final int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (parser.getName().equals(TAG_REMEMBERED_WIFI_DISPLAYS)) { loadRememberedWifiDisplaysFromXml(parser); } if (parser.getName().equals(TAG_DISPLAY_STATES)) { loadDisplaysFromXml(parser); } if (parser.getName().equals(TAG_STABLE_DEVICE_VALUES)) { mStableDeviceValues.loadFromXml(parser); } if (parser.getName().equals(TAG_BRIGHTNESS_CONFIGURATIONS)) { mBrightnessConfigurations.loadFromXml(parser); } } } private void loadRememberedWifiDisplaysFromXml(XmlPullParser parser) throws IOException, XmlPullParserException { final int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (parser.getName().equals(TAG_WIFI_DISPLAY)) { String deviceAddress = parser.getAttributeValue(null, ATTR_DEVICE_ADDRESS); String deviceName = parser.getAttributeValue(null, ATTR_DEVICE_NAME); String deviceAlias = parser.getAttributeValue(null, ATTR_DEVICE_ALIAS); if (deviceAddress == null || deviceName == null) { throw new XmlPullParserException( "Missing deviceAddress or deviceName attribute on wifi-display."); } if (findRememberedWifiDisplay(deviceAddress) >= 0) { throw new XmlPullParserException( "Found duplicate wifi display device address."); } mRememberedWifiDisplays.add( new WifiDisplay(deviceAddress, deviceName, deviceAlias, false, false, false)); } } } private void loadDisplaysFromXml(XmlPullParser parser) throws IOException, XmlPullParserException { final int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (parser.getName().equals(TAG_DISPLAY)) { String uniqueId = parser.getAttributeValue(null, ATTR_UNIQUE_ID); if (uniqueId == null) { throw new XmlPullParserException( "Missing unique-id attribute on display."); } if (mDisplayStates.containsKey(uniqueId)) { throw new XmlPullParserException("Found duplicate display."); } DisplayState state = new DisplayState(); state.loadFromXml(parser); mDisplayStates.put(uniqueId, state); } } } private void saveToXml(XmlSerializer serializer) throws IOException { serializer.startDocument(null, true); serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); serializer.startTag(null, TAG_DISPLAY_MANAGER_STATE); serializer.startTag(null, TAG_REMEMBERED_WIFI_DISPLAYS); for (WifiDisplay display : mRememberedWifiDisplays) { serializer.startTag(null, TAG_WIFI_DISPLAY); serializer.attribute(null, ATTR_DEVICE_ADDRESS, display.getDeviceAddress()); serializer.attribute(null, ATTR_DEVICE_NAME, display.getDeviceName()); if (display.getDeviceAlias() != null) { serializer.attribute(null, ATTR_DEVICE_ALIAS, display.getDeviceAlias()); } serializer.endTag(null, TAG_WIFI_DISPLAY); } serializer.endTag(null, TAG_REMEMBERED_WIFI_DISPLAYS); serializer.startTag(null, TAG_DISPLAY_STATES); for (Map.Entry entry : mDisplayStates.entrySet()) { final String uniqueId = entry.getKey(); final DisplayState state = entry.getValue(); serializer.startTag(null, TAG_DISPLAY); serializer.attribute(null, ATTR_UNIQUE_ID, uniqueId); state.saveToXml(serializer); serializer.endTag(null, TAG_DISPLAY); } serializer.endTag(null, TAG_DISPLAY_STATES); serializer.startTag(null, TAG_STABLE_DEVICE_VALUES); mStableDeviceValues.saveToXml(serializer); serializer.endTag(null, TAG_STABLE_DEVICE_VALUES); serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATIONS); mBrightnessConfigurations.saveToXml(serializer); serializer.endTag(null, TAG_BRIGHTNESS_CONFIGURATIONS); serializer.endTag(null, TAG_DISPLAY_MANAGER_STATE); serializer.endDocument(); } public void dump(PrintWriter pw) { pw.println("PersistentDataStore"); pw.println(" mLoaded=" + mLoaded); pw.println(" mDirty=" + mDirty); pw.println(" RememberedWifiDisplays:"); int i = 0; for (WifiDisplay display : mRememberedWifiDisplays) { pw.println(" " + i++ + ": " + display); } pw.println(" DisplayStates:"); i = 0; for (Map.Entry entry : mDisplayStates.entrySet()) { pw.println(" " + i++ + ": " + entry.getKey()); entry.getValue().dump(pw, " "); } pw.println(" StableDeviceValues:"); mStableDeviceValues.dump(pw, " "); pw.println(" BrightnessConfigurations:"); mBrightnessConfigurations.dump(pw, " "); } private static final class DisplayState { private int mColorMode; public boolean setColorMode(int colorMode) { if (colorMode == mColorMode) { return false; } mColorMode = colorMode; return true; } public int getColorMode() { return mColorMode; } public void loadFromXml(XmlPullParser parser) throws IOException, XmlPullParserException { final int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (parser.getName().equals(TAG_COLOR_MODE)) { String value = parser.nextText(); mColorMode = Integer.parseInt(value); } } } public void saveToXml(XmlSerializer serializer) throws IOException { serializer.startTag(null, TAG_COLOR_MODE); serializer.text(Integer.toString(mColorMode)); serializer.endTag(null, TAG_COLOR_MODE); } public void dump(final PrintWriter pw, final String prefix) { pw.println(prefix + "ColorMode=" + mColorMode); } } private static final class StableDeviceValues { private int mWidth; private int mHeight; private Point getDisplaySize() { return new Point(mWidth, mHeight); } public boolean setDisplaySize(Point r) { if (mWidth != r.x || mHeight != r.y) { mWidth = r.x; mHeight = r.y; return true; } return false; } public void loadFromXml(XmlPullParser parser) throws IOException, XmlPullParserException { final int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { switch (parser.getName()) { case TAG_STABLE_DISPLAY_WIDTH: mWidth = loadIntValue(parser); break; case TAG_STABLE_DISPLAY_HEIGHT: mHeight = loadIntValue(parser); break; } } } private static int loadIntValue(XmlPullParser parser) throws IOException, XmlPullParserException { try { String value = parser.nextText(); return Integer.parseInt(value); } catch (NumberFormatException nfe) { return 0; } } public void saveToXml(XmlSerializer serializer) throws IOException { if (mWidth > 0 && mHeight > 0) { serializer.startTag(null, TAG_STABLE_DISPLAY_WIDTH); serializer.text(Integer.toString(mWidth)); serializer.endTag(null, TAG_STABLE_DISPLAY_WIDTH); serializer.startTag(null, TAG_STABLE_DISPLAY_HEIGHT); serializer.text(Integer.toString(mHeight)); serializer.endTag(null, TAG_STABLE_DISPLAY_HEIGHT); } } public void dump(final PrintWriter pw, final String prefix) { pw.println(prefix + "StableDisplayWidth=" + mWidth); pw.println(prefix + "StableDisplayHeight=" + mHeight); } } private static final class BrightnessConfigurations { // Maps from a user ID to the users' given brightness configuration private SparseArray mConfigurations; // Timestamp of time the configuration was set. private SparseLongArray mTimeStamps; // Package that set the configuration. private SparseArray mPackageNames; public BrightnessConfigurations() { mConfigurations = new SparseArray<>(); mTimeStamps = new SparseLongArray(); mPackageNames = new SparseArray<>(); } private boolean setBrightnessConfigurationForUser(BrightnessConfiguration c, int userSerial, String packageName) { BrightnessConfiguration currentConfig = mConfigurations.get(userSerial); if (currentConfig != c && (currentConfig == null || !currentConfig.equals(c))) { if (c != null) { if (packageName == null) { mPackageNames.remove(userSerial); } else { mPackageNames.put(userSerial, packageName); } mTimeStamps.put(userSerial, System.currentTimeMillis()); mConfigurations.put(userSerial, c); } else { mPackageNames.remove(userSerial); mTimeStamps.delete(userSerial); mConfigurations.remove(userSerial); } return true; } return false; } public BrightnessConfiguration getBrightnessConfiguration(int userSerial) { return mConfigurations.get(userSerial); } public void loadFromXml(XmlPullParser parser) throws IOException, XmlPullParserException { final int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (TAG_BRIGHTNESS_CONFIGURATION.equals(parser.getName())) { int userSerial; try { userSerial = Integer.parseInt( parser.getAttributeValue(null, ATTR_USER_SERIAL)); } catch (NumberFormatException nfe) { userSerial = -1; Slog.e(TAG, "Failed to read in brightness configuration", nfe); } String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME); String timeStampString = parser.getAttributeValue(null, ATTR_TIME_STAMP); long timeStamp = -1; if (timeStampString != null) { try { timeStamp = Long.parseLong(timeStampString); } catch (NumberFormatException nfe) { // Ignore we will just not restore the timestamp. } } try { BrightnessConfiguration config = loadConfigurationFromXml(parser); if (userSerial >= 0 && config != null) { mConfigurations.put(userSerial, config); if (timeStamp != -1) { mTimeStamps.put(userSerial, timeStamp); } if (packageName != null) { mPackageNames.put(userSerial, packageName); } } } catch (IllegalArgumentException iae) { Slog.e(TAG, "Failed to load brightness configuration!", iae); } } } } private static BrightnessConfiguration loadConfigurationFromXml(XmlPullParser parser) throws IOException, XmlPullParserException { final int outerDepth = parser.getDepth(); String description = null; Pair curve = null; while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (TAG_BRIGHTNESS_CURVE.equals(parser.getName())) { description = parser.getAttributeValue(null, ATTR_DESCRIPTION); curve = loadCurveFromXml(parser); } } if (curve == null) { return null; } final BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder( curve.first, curve.second); builder.setDescription(description); return builder.build(); } private static Pair loadCurveFromXml(XmlPullParser parser) throws IOException, XmlPullParserException { final int outerDepth = parser.getDepth(); List luxLevels = new ArrayList<>(); List nitLevels = new ArrayList<>(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (TAG_BRIGHTNESS_POINT.equals(parser.getName())) { luxLevels.add(loadFloat(parser.getAttributeValue(null, ATTR_LUX))); nitLevels.add(loadFloat(parser.getAttributeValue(null, ATTR_NITS))); } } final int N = luxLevels.size(); float[] lux = new float[N]; float[] nits = new float[N]; for (int i = 0; i < N; i++) { lux[i] = luxLevels.get(i); nits[i] = nitLevels.get(i); } return Pair.create(lux, nits); } private static float loadFloat(String val) { try { return Float.parseFloat(val); } catch (NullPointerException | NumberFormatException e) { Slog.e(TAG, "Failed to parse float loading brightness config", e); return Float.NEGATIVE_INFINITY; } } public void saveToXml(XmlSerializer serializer) throws IOException { for (int i = 0; i < mConfigurations.size(); i++) { final int userSerial = mConfigurations.keyAt(i); final BrightnessConfiguration config = mConfigurations.valueAt(i); serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATION); serializer.attribute(null, ATTR_USER_SERIAL, Integer.toString(userSerial)); String packageName = mPackageNames.get(userSerial); if (packageName != null) { serializer.attribute(null, ATTR_PACKAGE_NAME, packageName); } long timestamp = mTimeStamps.get(userSerial, -1); if (timestamp != -1) { serializer.attribute(null, ATTR_TIME_STAMP, Long.toString(timestamp)); } saveConfigurationToXml(serializer, config); serializer.endTag(null, TAG_BRIGHTNESS_CONFIGURATION); } } private static void saveConfigurationToXml(XmlSerializer serializer, BrightnessConfiguration config) throws IOException { serializer.startTag(null, TAG_BRIGHTNESS_CURVE); if (config.getDescription() != null) { serializer.attribute(null, ATTR_DESCRIPTION, config.getDescription()); } final Pair curve = config.getCurve(); for (int i = 0; i < curve.first.length; i++) { serializer.startTag(null, TAG_BRIGHTNESS_POINT); serializer.attribute(null, ATTR_LUX, Float.toString(curve.first[i])); serializer.attribute(null, ATTR_NITS, Float.toString(curve.second[i])); serializer.endTag(null, TAG_BRIGHTNESS_POINT); } serializer.endTag(null, TAG_BRIGHTNESS_CURVE); } public void dump(final PrintWriter pw, final String prefix) { for (int i = 0; i < mConfigurations.size(); i++) { final int userSerial = mConfigurations.keyAt(i); long time = mTimeStamps.get(userSerial, -1); String packageName = mPackageNames.get(userSerial); pw.println(prefix + "User " + userSerial + ":"); if (time != -1) { pw.println(prefix + " set at: " + TimeUtils.formatForLogging(time)); } if (packageName != null) { pw.println(prefix + " set by: " + packageName); } pw.println(prefix + " " + mConfigurations.valueAt(i)); } } } @VisibleForTesting static class Injector { private final AtomicFile mAtomicFile; public Injector() { mAtomicFile = new AtomicFile(new File("/data/system/display-manager-state.xml"), "display-state"); } public InputStream openRead() throws FileNotFoundException { return mAtomicFile.openRead(); } public OutputStream startWrite() throws IOException { return mAtomicFile.startWrite(); } public void finishWrite(OutputStream os, boolean success) { if (!(os instanceof FileOutputStream)) { throw new IllegalArgumentException("Unexpected OutputStream as argument: " + os); } FileOutputStream fos = (FileOutputStream) os; if (success) { mAtomicFile.finishWrite(fos); } else { mAtomicFile.failWrite(fos); } } } }