diff options
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)); + } +} |