summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/system-current.txt1
-rw-r--r--core/java/android/hardware/display/ColorDisplayManager.java28
-rw-r--r--core/java/android/hardware/display/IColorDisplayManager.aidl1
-rw-r--r--services/core/java/com/android/server/display/AppSaturationController.java213
-rw-r--r--services/core/java/com/android/server/display/ColorDisplayService.java68
-rw-r--r--services/tests/servicestests/src/com/android/server/display/AppSaturationControllerTest.java115
6 files changed, 423 insertions, 3 deletions
diff --git a/api/system-current.txt b/api/system-current.txt
index 7ea7afbb8d20..731f3da7cddd 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -1757,6 +1757,7 @@ package android.hardware.display {
}
public final class ColorDisplayManager {
+ method public boolean setAppSaturationLevel(java.lang.String, int);
method public boolean setSaturationLevel(int);
}
diff --git a/core/java/android/hardware/display/ColorDisplayManager.java b/core/java/android/hardware/display/ColorDisplayManager.java
index 0cf2d18e5c04..aefcf8c8d011 100644
--- a/core/java/android/hardware/display/ColorDisplayManager.java
+++ b/core/java/android/hardware/display/ColorDisplayManager.java
@@ -17,6 +17,8 @@
package android.hardware.display;
import android.Manifest;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
@@ -61,16 +63,30 @@ public final class ColorDisplayManager {
*
* @param saturationLevel 0-100 (inclusive), where 100 is full saturation
* @return whether the saturation level change was applied successfully
- *
* @hide
*/
@SystemApi
@RequiresPermission(Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS)
- public boolean setSaturationLevel(int saturationLevel) {
+ public boolean setSaturationLevel(@IntRange(from = 0, to = 100) int saturationLevel) {
return mManager.setSaturationLevel(saturationLevel);
}
/**
+ * Set the level of color saturation to apply to a specific app.
+ *
+ * @param packageName the package name of the app whose windows should be desaturated
+ * @param saturationLevel 0-100 (inclusive), where 100 is full saturation
+ * @return whether the saturation level change was applied successfully
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS)
+ public boolean setAppSaturationLevel(@NonNull String packageName,
+ @IntRange(from = 0, to = 100) int saturationLevel) {
+ return mManager.setAppSaturationLevel(packageName, saturationLevel);
+ }
+
+ /**
* Returns {@code true} if Night Display is supported by the device.
*
* @hide
@@ -128,5 +144,13 @@ public final class ColorDisplayManager {
throw e.rethrowFromSystemServer();
}
}
+
+ boolean setAppSaturationLevel(String packageName, int saturationLevel) {
+ try {
+ return mCdm.setAppSaturationLevel(packageName, saturationLevel);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
}
diff --git a/core/java/android/hardware/display/IColorDisplayManager.aidl b/core/java/android/hardware/display/IColorDisplayManager.aidl
index 81b82c64461f..644f510d45f9 100644
--- a/core/java/android/hardware/display/IColorDisplayManager.aidl
+++ b/core/java/android/hardware/display/IColorDisplayManager.aidl
@@ -21,4 +21,5 @@ interface IColorDisplayManager {
boolean isDeviceColorManaged();
boolean setSaturationLevel(int saturationLevel);
+ boolean setAppSaturationLevel(String packageName, int saturationLevel);
} \ No newline at end of file
diff --git a/services/core/java/com/android/server/display/AppSaturationController.java b/services/core/java/com/android/server/display/AppSaturationController.java
new file mode 100644
index 000000000000..5d5e4f70ea7f
--- /dev/null
+++ b/services/core/java/com/android/server/display/AppSaturationController.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2019 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.UserIdInt;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.display.ColorDisplayService.ColorTransformController;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+class AppSaturationController {
+
+ private final Object mLock = new Object();
+
+ /**
+ * A package name has one or more userIds it is running under. Each userId has zero or one
+ * saturation level, and zero or more ColorTransformControllers.
+ */
+ @GuardedBy("mLock")
+ private final Map<String, SparseArray<SaturationController>> mAppsMap = new HashMap<>();
+
+ @VisibleForTesting
+ static final float[] TRANSLATION_VECTOR = {0f, 0f, 0f};
+
+ /**
+ * Add an {@link WeakReference<ColorTransformController>} for a given package and userId.
+ */
+ boolean addColorTransformController(String packageName, @UserIdInt int userId,
+ WeakReference<ColorTransformController> controller) {
+ synchronized (mLock) {
+ return getSaturationControllerLocked(packageName, userId)
+ .addColorTransformController(controller);
+ }
+ }
+
+ /**
+ * Set the saturation level ({@code ColorDisplayManager#SaturationLevel} constant for a given
+ * package name and userId.
+ */
+ public boolean setSaturationLevel(String packageName, @UserIdInt int userId,
+ int saturationLevel) {
+ synchronized (mLock) {
+ return getSaturationControllerLocked(packageName, userId)
+ .setSaturationLevel(saturationLevel);
+ }
+ }
+
+ /**
+ * Dump state information.
+ */
+ public void dump(PrintWriter pw) {
+ synchronized (mLock) {
+ pw.println("App Saturation: ");
+ if (mAppsMap.size() == 0) {
+ pw.println(" No packages");
+ return;
+ }
+ final List<String> packageNames = new ArrayList<>(mAppsMap.keySet());
+ Collections.sort(packageNames);
+ for (String packageName : packageNames) {
+ pw.println(" " + packageName + ":");
+ final SparseArray<SaturationController> appUserIdMap = mAppsMap.get(packageName);
+ for (int i = 0; i < appUserIdMap.size(); i++) {
+ pw.println(" " + appUserIdMap.keyAt(i) + ":");
+ appUserIdMap.valueAt(i).dump(pw);
+ }
+ }
+ }
+ }
+
+ /**
+ * Retrieve the SaturationController for a given package and userId, creating all intermediate
+ * connections as needed.
+ */
+ private SaturationController getSaturationControllerLocked(String packageName,
+ @UserIdInt int userId) {
+ return getOrCreateSaturationControllerLocked(getOrCreateUserIdMapLocked(packageName),
+ userId);
+ }
+
+ /**
+ * Retrieve or create the mapping between the app's given package name and its userIds (and
+ * their SaturationControllers).
+ */
+ private SparseArray<SaturationController> getOrCreateUserIdMapLocked(String packageName) {
+ if (mAppsMap.get(packageName) != null) {
+ return mAppsMap.get(packageName);
+ }
+
+ final SparseArray<SaturationController> appUserIdMap = new SparseArray<>();
+ mAppsMap.put(packageName, appUserIdMap);
+ return appUserIdMap;
+ }
+
+ /**
+ * Retrieve or create the mapping between an app's given userId and SaturationController.
+ */
+ private SaturationController getOrCreateSaturationControllerLocked(
+ SparseArray<SaturationController> appUserIdMap, @UserIdInt int userId) {
+ if (appUserIdMap.get(userId) != null) {
+ return appUserIdMap.get(userId);
+ }
+
+ final SaturationController saturationController = new SaturationController();
+ appUserIdMap.put(userId, saturationController);
+ return saturationController;
+ }
+
+ @VisibleForTesting
+ static void computeGrayscaleTransformMatrix(float saturation, float[] matrix) {
+ float desaturation = 1.0f - saturation;
+ float[] luminance = {0.231f * desaturation, 0.715f * desaturation,
+ 0.072f * desaturation};
+ matrix[0] = luminance[0] + saturation;
+ matrix[1] = luminance[0];
+ matrix[2] = luminance[0];
+ matrix[3] = luminance[1];
+ matrix[4] = luminance[1] + saturation;
+ matrix[5] = luminance[1];
+ matrix[6] = luminance[2];
+ matrix[7] = luminance[2];
+ matrix[8] = luminance[2] + saturation;
+ }
+
+ private static class SaturationController {
+
+ private final List<WeakReference<ColorTransformController>> mControllerRefs =
+ new ArrayList<>();
+ private int mSaturationLevel = 100;
+ private float[] mTransformMatrix = new float[9];
+
+ private boolean setSaturationLevel(int saturationLevel) {
+ mSaturationLevel = saturationLevel;
+ if (!mControllerRefs.isEmpty()) {
+ return updateState();
+ }
+ return false;
+ }
+
+ private boolean addColorTransformController(
+ WeakReference<ColorTransformController> controller) {
+ mControllerRefs.add(controller);
+ if (mSaturationLevel != 100) {
+ return updateState();
+ } else {
+ clearExpiredReferences();
+ }
+ return false;
+ }
+
+ private boolean updateState() {
+ computeGrayscaleTransformMatrix(mSaturationLevel / 100f, mTransformMatrix);
+
+ boolean updated = false;
+ final Iterator<WeakReference<ColorTransformController>> iterator = mControllerRefs
+ .iterator();
+ while (iterator.hasNext()) {
+ WeakReference<ColorTransformController> controllerRef = iterator.next();
+ final ColorTransformController controller = controllerRef.get();
+ if (controller != null) {
+ controller.applyAppSaturation(mTransformMatrix, TRANSLATION_VECTOR);
+ updated = true;
+ } else {
+ // Purge cleared refs lazily to avoid accumulating a lot of dead windows
+ iterator.remove();
+ }
+ }
+ return updated;
+
+ }
+
+ private void clearExpiredReferences() {
+ final Iterator<WeakReference<ColorTransformController>> iterator = mControllerRefs
+ .iterator();
+ while (iterator.hasNext()) {
+ WeakReference<ColorTransformController> controllerRef = iterator.next();
+ final ColorTransformController controller = controllerRef.get();
+ if (controller == null) {
+ iterator.remove();
+ }
+ }
+ }
+
+ private void dump(PrintWriter pw) {
+ pw.println(" mSaturationLevel: " + mSaturationLevel);
+ pw.println(" mControllerRefs count: " + mControllerRefs.size());
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/display/ColorDisplayService.java b/services/core/java/com/android/server/display/ColorDisplayService.java
index 2fe17d8f3c98..f06cf0b3a87c 100644
--- a/services/core/java/com/android/server/display/ColorDisplayService.java
+++ b/services/core/java/com/android/server/display/ColorDisplayService.java
@@ -27,6 +27,7 @@ import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.Size;
import android.app.AlarmManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
@@ -36,8 +37,8 @@ import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.ContentObserver;
-import android.hardware.display.ColorDisplayManager;
import android.graphics.ColorSpace;
+import android.hardware.display.ColorDisplayManager;
import android.hardware.display.IColorDisplayManager;
import android.net.Uri;
import android.opengl.Matrix;
@@ -55,13 +56,16 @@ import android.view.animation.AnimationUtils;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.ColorDisplayController;
+import com.android.internal.util.DumpUtils;
import com.android.server.DisplayThread;
import com.android.server.SystemService;
import com.android.server.twilight.TwilightListener;
import com.android.server.twilight.TwilightManager;
import com.android.server.twilight.TwilightState;
+import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
@@ -400,6 +404,8 @@ public final class ColorDisplayService extends SystemService {
private final Handler mHandler;
+ private final AppSaturationController mAppSaturationController = new AppSaturationController();
+
private int mCurrentUser = UserHandle.USER_NULL;
private ContentObserver mUserSetupObserver;
private boolean mBootCompleted;
@@ -829,6 +835,22 @@ public final class ColorDisplayService extends SystemService {
return LocalDateTime.MIN;
}
+ private boolean setAppSaturationLevelInternal(String packageName, int saturationLevel) {
+ return mAppSaturationController
+ .setSaturationLevel(packageName, mCurrentUser, saturationLevel);
+ }
+
+ private void dumpInternal(PrintWriter pw) {
+ pw.println("COLOR DISPLAY MANAGER dumpsys (color_display)");
+ pw.println("Night Display:");
+ if (ColorDisplayManager.isNightDisplayAvailable(getContext())) {
+ pw.println(" Activated: " + mNightDisplayTintController.isActivated());
+ } else {
+ pw.println(" Not available");
+ }
+ mAppSaturationController.dump(pw);
+ }
+
private abstract class NightDisplayAutoMode {
public abstract void onActivated(boolean activated);
@@ -1132,6 +1154,16 @@ public final class ColorDisplayService extends SystemService {
public void dump(PrintWriter pw) {
mDisplayWhiteBalanceTintController.dump(pw);
}
+
+ /**
+ * Adds a {@link WeakReference<ColorTransformController>} for a newly started activity, and
+ * invokes {@link ColorTransformController#applyAppSaturation(float[], float[])} if needed.
+ */
+ public boolean attachColorTransformController(String packageName, int uid,
+ WeakReference<ColorTransformController> controller) {
+ return mAppSaturationController
+ .addColorTransformController(packageName, uid, controller);
+ }
}
/**
@@ -1163,6 +1195,15 @@ public final class ColorDisplayService extends SystemService {
}
}
+ /**
+ * Interface for applying transforms to a given AppWindow.
+ */
+ public interface ColorTransformController {
+
+ /** Apply the given saturation (grayscale) matrix to the associated AppWindow. */
+ void applyAppSaturation(@Size(9) float[] matrix, @Size(3) float[] translation);
+ }
+
private final class BinderService extends IColorDisplayManager.Stub {
@Override
@@ -1196,5 +1237,30 @@ public final class ColorDisplayService extends SystemService {
}
return true;
}
+
+ @Override
+ public boolean setAppSaturationLevel(String packageName, int level) {
+ getContext().enforceCallingPermission(
+ Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS,
+ "Permission required to set display saturation level");
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return setAppSaturationLevelInternal(packageName, level);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return;
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ dumpInternal(pw);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/display/AppSaturationControllerTest.java b/services/tests/servicestests/src/com/android/server/display/AppSaturationControllerTest.java
new file mode 100644
index 000000000000..e51884429718
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/display/AppSaturationControllerTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2019 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 static com.android.server.display.AppSaturationController.TRANSLATION_VECTOR;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.ActivityManager;
+import android.os.UserHandle;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.display.ColorDisplayService.ColorTransformController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.lang.ref.WeakReference;
+
+@RunWith(AndroidJUnit4.class)
+public class AppSaturationControllerTest {
+
+ private static final String TEST_PACKAGE_NAME = "com.android.test";
+
+ private int mUserId;
+ private AppSaturationController mAppSaturationController;
+ private float[] mMatrix;
+
+ @Mock
+ private ColorTransformController mColorTransformController;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mUserId = ActivityManager.getCurrentUser();
+ mAppSaturationController = new AppSaturationController();
+ mMatrix = new float[9];
+ }
+
+ @After
+ public void tearDown() {
+ mAppSaturationController = null;
+ mUserId = UserHandle.USER_NULL;
+ }
+
+ @Test
+ public void addColorTransformController_appliesExistingSaturation() {
+ final WeakReference<ColorTransformController> ref = new WeakReference<>(
+ mColorTransformController);
+ mAppSaturationController.setSaturationLevel(TEST_PACKAGE_NAME, mUserId, 30);
+ mAppSaturationController.addColorTransformController(TEST_PACKAGE_NAME, mUserId, ref);
+ AppSaturationController.computeGrayscaleTransformMatrix(.3f, mMatrix);
+ verify(mColorTransformController).applyAppSaturation(eq(mMatrix), eq(TRANSLATION_VECTOR));
+ }
+
+ @Test
+ public void setSaturationLevel_resetToDefault() {
+ final WeakReference<ColorTransformController> ref = new WeakReference<>(
+ mColorTransformController);
+ mAppSaturationController.addColorTransformController(TEST_PACKAGE_NAME, mUserId, ref);
+ verify(mColorTransformController, never())
+ .applyAppSaturation(any(), eq(TRANSLATION_VECTOR));
+ mAppSaturationController.setSaturationLevel(TEST_PACKAGE_NAME, mUserId, 30);
+ AppSaturationController.computeGrayscaleTransformMatrix(.3f, mMatrix);
+ verify(mColorTransformController, times(1))
+ .applyAppSaturation(eq(mMatrix), eq(TRANSLATION_VECTOR));
+ mAppSaturationController.setSaturationLevel(TEST_PACKAGE_NAME, mUserId, 100);
+ AppSaturationController.computeGrayscaleTransformMatrix(1.0f, mMatrix);
+ verify(mColorTransformController, times(2))
+ .applyAppSaturation(eq(mMatrix), eq(TRANSLATION_VECTOR));
+ }
+
+ @Test
+ public void setSaturationLevel_updateLevel() {
+ final WeakReference<ColorTransformController> ref = new WeakReference<>(
+ mColorTransformController);
+ mAppSaturationController.addColorTransformController(TEST_PACKAGE_NAME, mUserId, ref);
+ verify(mColorTransformController, never())
+ .applyAppSaturation(any(), eq(TRANSLATION_VECTOR));
+ mAppSaturationController.setSaturationLevel(TEST_PACKAGE_NAME, mUserId, 30);
+ AppSaturationController.computeGrayscaleTransformMatrix(.3f, mMatrix);
+ verify(mColorTransformController).applyAppSaturation(eq(mMatrix), eq(TRANSLATION_VECTOR));
+ mAppSaturationController.setSaturationLevel(TEST_PACKAGE_NAME, mUserId, 70);
+ AppSaturationController.computeGrayscaleTransformMatrix(.7f, mMatrix);
+ verify(mColorTransformController, times(2))
+ .applyAppSaturation(eq(mMatrix), eq(TRANSLATION_VECTOR));
+ mAppSaturationController.setSaturationLevel(TEST_PACKAGE_NAME, mUserId, 100);
+ AppSaturationController.computeGrayscaleTransformMatrix(1.0f, mMatrix);
+ verify(mColorTransformController, times(3))
+ .applyAppSaturation(eq(mMatrix), eq(TRANSLATION_VECTOR));
+ }
+}