Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cbbc772a authored by Aurélien Pomini's avatar Aurélien Pomini
Browse files

Add the contrast API to UiModeManager

Part of the move of the API from AccessibilityManager to UiModeManager.
The logic remains unchained. a new AIDL, IUiModeManagerCallback, was
created to let the service notify all UiModeManagers about contrast
changes, similarly to what was done in AccessibilityManager

Constants (min, max, default contrast) have been moved to a new static
class "ContrastUtils" inside "UiModeManager".

To avoid creating too many CLs, a few new hidden constants/utils have
been added to "ContrastUtils", even though this is not part of the move.

Test: atest UiModeManagerTest
Bug: 259091608

Change-Id: I0c1cf5accc8b301ea90b81cdc20d5262eef04a8b
parent c7575771
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -7398,12 +7398,15 @@ package android.app {
  }
  public class UiModeManager {
    method public void addContrastChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.app.UiModeManager.ContrastChangeListener);
    method public void disableCarMode(int);
    method public void enableCarMode(int);
    method @FloatRange(from=-1.0F, to=1.0f) public float getContrast();
    method public int getCurrentModeType();
    method @NonNull public java.time.LocalTime getCustomNightModeEnd();
    method @NonNull public java.time.LocalTime getCustomNightModeStart();
    method public int getNightMode();
    method public void removeContrastChangeListener(@NonNull android.app.UiModeManager.ContrastChangeListener);
    method public void setApplicationNightMode(int);
    method public void setCustomNightModeEnd(@NonNull java.time.LocalTime);
    method public void setCustomNightModeStart(@NonNull java.time.LocalTime);
@@ -7421,6 +7424,10 @@ package android.app {
    field public static final int MODE_NIGHT_YES = 2; // 0x2
  }
  public static interface UiModeManager.ContrastChangeListener {
    method public void onContrastChanged(@FloatRange(from=-1.0F, to=1.0f) float);
  }
  public final class VoiceInteractor {
    method public android.app.VoiceInteractor.Request getActiveRequest(String);
    method public android.app.VoiceInteractor.Request[] getActiveRequests();
+11 −0
Original line number Diff line number Diff line
@@ -17,12 +17,18 @@
package android.app;

import android.app.IOnProjectionStateChangedListener;
import android.app.IUiModeManagerCallback;

/**
 * Interface used to control special UI modes.
 * @hide
 */
interface IUiModeManager {
    /**
     * @hide
     */
    void addCallback(IUiModeManagerCallback callback);

    /**
     * Enables the car mode. Only the system can do this.
     * @hide
@@ -173,4 +179,9 @@ interface IUiModeManager {
    * Returns currently set projection types.
    */
    int getActiveProjectionTypes();

    /**
    * Returns the contrast for the current user
    */
    float getContrast();
}
+27 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 android.app;

/**
* Implemented by the UiModeManager client to receive information about changes from the service.
* This is a oneway interface since the server should not block waiting for the client.
*
* @hide
*/
oneway interface IUiModeManagerCallback {
  void notifyContrastChanged(float contrast);
}
+170 −1
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package android.app;

import android.annotation.CallbackExecutor;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -34,6 +35,7 @@ import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
@@ -43,10 +45,13 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.time.LocalTime;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.stream.Stream;

/**
 * This class provides access to the system uimode services.  These services
@@ -72,6 +77,10 @@ import java.util.concurrent.Executor;
 */
@SystemService(Context.UI_MODE_SERVICE)
public class UiModeManager {

    private static final String TAG = "UiModeManager";


    /**
     * A listener with a single method that is invoked whenever the packages projecting using the
     * {@link ProjectionType}s for which it is registered change.
@@ -91,7 +100,20 @@ public class UiModeManager {
                @NonNull Set<String> packageNames);
    }

    private static final String TAG = "UiModeManager";
    /**
     * Listener for the UI contrast. To listen for changes to
     * the UI contrast on the device, implement this interface and
     * register it with the system by calling {@link #addContrastChangeListener}.
     */
    public interface ContrastChangeListener {

        /**
         * Called when the color contrast enabled state changes.
         *
         * @param contrast The color contrast as in {@link #getContrast}
         */
        void onContrastChanged(@FloatRange(from = -1.0f, to = 1.0f) float contrast);
    }

    /**
     * Broadcast sent when the device's UI has switched to car mode, either
@@ -319,6 +341,95 @@ public class UiModeManager {
            mOnProjectionStateChangedListenerResourceManager =
            new OnProjectionStateChangedListenerResourceManager();

    /**
     * Define constants and conversions between {@link ContrastLevel}s and contrast values.
     * <p>
     * Contrast values are floats defined in [-1, 1], as defined in {@link #getContrast}.
     * This is the official data type for contrast;
     * all methods from the public API return contrast values.
     * </p>
     * <p>
     * {@code ContrastLevel}, on the other hand, is an internal-only enumeration of contrasts that
     * can be set from the system ui. Each {@code ContrastLevel} has an associated contrast value.
     * </p>
     * <p>
     * Currently, a user chan chose from three contrast levels:
     * <ul>
     *     <li>{@link #CONTRAST_LEVEL_STANDARD}, corresponding to the default contrast value 0f</li>
     *     <li>{@link #CONTRAST_LEVEL_MEDIUM}, corresponding to the contrast value 0.5f</li>
     *     <li>{@link #CONTRAST_LEVEL_HIGH}, corresponding to the maximum contrast value 1f</li>
     * </ul>
     * </p>
     *
     * @hide
     */
    public static class ContrastUtils {

        private static final float CONTRAST_MIN_VALUE = -1f;
        private static final float CONTRAST_MAX_VALUE = 1f;
        public static final float CONTRAST_DEFAULT_VALUE = 0f;

        @IntDef(flag = true, prefix = { "CONTRAST_LEVEL_" }, value = {
                CONTRAST_LEVEL_STANDARD,
                CONTRAST_LEVEL_MEDIUM,
                CONTRAST_LEVEL_HIGH
        })
        @Retention(RetentionPolicy.SOURCE)
        public @interface ContrastLevel {}

        public static final int CONTRAST_LEVEL_STANDARD = 0;
        public static final int CONTRAST_LEVEL_MEDIUM = 1;
        public static final int CONTRAST_LEVEL_HIGH = 2;

        private static Stream<Integer> allContrastLevels() {
            return Stream.of(CONTRAST_LEVEL_STANDARD, CONTRAST_LEVEL_MEDIUM, CONTRAST_LEVEL_HIGH);
        }

        /**
         * Convert a contrast value in [-1, 1] to its associated {@link ContrastLevel}
         */
        public static @ContrastLevel int toContrastLevel(float contrast) {
            if (contrast < CONTRAST_MIN_VALUE || contrast > CONTRAST_MAX_VALUE) {
                throw new IllegalArgumentException("contrast values should be in [-1, 1]");
            }
            return allContrastLevels().min(Comparator.comparingDouble(contrastLevel ->
                    Math.abs(contrastLevel - 2 * contrast))).orElseThrow();
        }

        /**
         * Convert a {@link ContrastLevel} to its associated contrast value in [-1, 1]
         */
        public static float fromContrastLevel(@ContrastLevel int contrastLevel) {
            if (allContrastLevels().noneMatch(level -> level == contrastLevel)) {
                throw new IllegalArgumentException("unrecognized contrast level: " + contrastLevel);
            }
            return contrastLevel / 2f;
        }
    }

    /**
     * Map that stores user provided {@link ContrastChangeListener} callbacks,
     * and the executors on which these callbacks should be called.
     */
    private final ArrayMap<ContrastChangeListener, Executor>
            mContrastChangeListeners = new ArrayMap<>();
    private float mContrast;

    private final IUiModeManagerCallback.Stub mCallback = new IUiModeManagerCallback.Stub() {
        @Override
        public void notifyContrastChanged(float contrast) {
            final ArrayMap<ContrastChangeListener, Executor> listeners;
            synchronized (mLock) {
                // if value changed in the settings, update the cached value and notify listeners
                if (Math.abs(mContrast - contrast) < 1e-10) return;
                mContrast = contrast;
                listeners = new ArrayMap<>(mContrastChangeListeners);
            }
            listeners.forEach((listener, executor) -> executor.execute(
                    () -> listener.onContrastChanged(mContrast)));
        }
    };

    @UnsupportedAppUsage
    /*package*/ UiModeManager() throws ServiceNotFoundException {
        this(null /* context */);
@@ -328,6 +439,12 @@ public class UiModeManager {
        mService = IUiModeManager.Stub.asInterface(
                ServiceManager.getServiceOrThrow(Context.UI_MODE_SERVICE));
        mContext = context;
        try {
            mService.addCallback(mCallback);
            mContrast = mService.getContrast();
        } catch (RemoteException e) {
            Log.e(TAG, "Setup failed: UiModeManagerService is dead", e);
        }
    }

    /**
@@ -1067,4 +1184,56 @@ public class UiModeManager {
            return mExecutorMap.get(innerListener);
        }
    }

    /**
     * Returns the color contrast for the user.
     * <p>
     * <strong>Note:</strong> You need to query this only if your application is
     * doing its own rendering and does not rely on the material rendering pipeline.
     * </p>
     * @return The color contrast, float in [-1, 1] where
     * <ul>
     *     <li> &nbsp; 0 corresponds to the default contrast </li>
     *     <li>       -1 corresponds to the minimum contrast </li>
     *     <li> &nbsp; 1 corresponds to the maximum contrast </li>
     * </ul>
     *
     *
     *
     */
    @FloatRange(from = -1.0f, to = 1.0f)
    public float getContrast() {
        synchronized (mLock) {
            return mContrast;
        }
    }

    /**
     * Registers a {@link ContrastChangeListener} for the current user.
     *
     * @param executor The executor on which the listener should be called back.
     * @param listener The listener.
     */
    public void addContrastChangeListener(
            @NonNull @CallbackExecutor Executor executor,
            @NonNull ContrastChangeListener listener) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(listener);
        synchronized (mLock) {
            mContrastChangeListeners.put(listener, executor);
        }
    }

    /**
     * Unregisters a {@link ContrastChangeListener} for the current user.
     * If the listener was not registered, does nothing and returns.
     *
     * @param listener The listener to unregister.
     */
    public void removeContrastChangeListener(@NonNull ContrastChangeListener listener) {
        Objects.requireNonNull(listener);
        synchronized (mLock) {
            mContrastChangeListeners.remove(listener);
        }
    }
}
+67 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server;

import static android.app.UiModeManager.ContrastUtils.CONTRAST_DEFAULT_VALUE;
import static android.app.UiModeManager.DEFAULT_PRIORITY;
import static android.app.UiModeManager.MODE_NIGHT_AUTO;
import static android.app.UiModeManager.MODE_NIGHT_CUSTOM;
@@ -27,8 +28,11 @@ import static android.app.UiModeManager.MODE_NIGHT_YES;
import static android.app.UiModeManager.PROJECTION_TYPE_AUTOMOTIVE;
import static android.app.UiModeManager.PROJECTION_TYPE_NONE;
import static android.os.UserHandle.USER_SYSTEM;
import static android.provider.Settings.Secure.CONTRAST_LEVEL;
import static android.util.TimeUtils.isTimeBetween;

import static com.android.internal.util.FunctionalUtils.ignoreRemoteException;

import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -38,6 +42,7 @@ import android.app.ActivityTaskManager;
import android.app.AlarmManager;
import android.app.IOnProjectionStateChangedListener;
import android.app.IUiModeManager;
import android.app.IUiModeManagerCallback;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationManager;
@@ -71,6 +76,7 @@ import android.os.ShellCallback;
import android.os.ShellCommand;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.service.dreams.Sandman;
import android.service.vr.IVrManager;
@@ -192,6 +198,10 @@ final class UiModeManagerService extends SystemService {
    private final LocalService mLocalService = new LocalService();
    private PowerManagerInternal mLocalPowerManager;

    @GuardedBy("mLock")
    private final RemoteCallbackList<IUiModeManagerCallback> mUiModeManagerCallbacks =
            new RemoteCallbackList<IUiModeManagerCallback>();

    @GuardedBy("mLock")
    @Nullable
    private SparseArray<List<ProjectionHolder>> mProjectionHolders;
@@ -199,6 +209,9 @@ final class UiModeManagerService extends SystemService {
    @Nullable
    private SparseArray<RemoteCallbackList<IOnProjectionStateChangedListener>> mProjectionListeners;

    @GuardedBy("mLock")
    private final SparseArray<Float> mContrasts = new SparseArray<>();

    public UiModeManagerService(Context context) {
        this(context, /* setupWizardComplete= */ false, /* tm= */ null, new Injector());
    }
@@ -352,6 +365,19 @@ final class UiModeManagerService extends SystemService {
        }
    };

    private final ContentObserver mContrastObserver = new ContentObserver(mHandler) {
        @Override
        public void onChange(boolean selfChange, Uri uri) {
            synchronized (mLock) {
                if (updateContrastLocked()) {
                    float contrast = getContrastLocked();
                    mUiModeManagerCallbacks.broadcast(ignoreRemoteException(callback ->
                            callback.notifyContrastChanged(contrast)));
                }
            }
        }
    };

    private void updateSystemProperties() {
        int mode = Secure.getIntForUser(getContext().getContentResolver(), Secure.UI_NIGHT_MODE,
                mNightMode, 0);
@@ -407,6 +433,9 @@ final class UiModeManagerService extends SystemService {
                context.getContentResolver()
                        .registerContentObserver(Secure.getUriFor(Secure.UI_NIGHT_MODE),
                                false, mDarkThemeObserver, 0);
                context.getContentResolver().registerContentObserver(
                        Secure.getUriFor(Secure.CONTRAST_LEVEL), false,
                        mContrastObserver, UserHandle.USER_ALL);
                context.registerReceiver(mDockModeReceiver,
                        new IntentFilter(Intent.ACTION_DOCK_EVENT));
                IntentFilter batteryFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
@@ -633,6 +662,13 @@ final class UiModeManagerService extends SystemService {
    }

    private final IUiModeManager.Stub mService = new IUiModeManager.Stub() {
        @Override
        public void addCallback(IUiModeManagerCallback callback) {
            synchronized (mLock) {
                mUiModeManagerCallbacks.register(callback);
            }
        }

        @Override
        public void enableCarMode(@UiModeManager.EnableCarMode int flags,
                @IntRange(from = 0) int priority, String callingPackage) {
@@ -1132,6 +1168,13 @@ final class UiModeManagerService extends SystemService {
                }
            }
        }

        @Override
        public float getContrast() {
            synchronized (mLock) {
                return getContrastLocked();
            }
        }
    };

    private void enforceProjectionTypePermissions(@UiModeManager.ProjectionType int p) {
@@ -1214,6 +1257,30 @@ final class UiModeManagerService extends SystemService {
        }
    }

    /**
     * Return the contrast for the current user. If not cached, fetch it from the settings.
     */
    @GuardedBy("mLock")
    private float getContrastLocked() {
        if (!mContrasts.contains(mCurrentUser)) updateContrastLocked();
        return mContrasts.get(mCurrentUser);
    }

    /**
     * Read the contrast setting for the current user and update {@link #mContrasts}
     * if the contrast changed. Returns true if {@link #mContrasts} was updated.
     */
    @GuardedBy("mLock")
    private boolean updateContrastLocked() {
        float contrast = Settings.Secure.getFloatForUser(getContext().getContentResolver(),
                CONTRAST_LEVEL, CONTRAST_DEFAULT_VALUE, mCurrentUser);
        if (Math.abs(mContrasts.get(mCurrentUser, Float.MAX_VALUE) - contrast) >= 1e-10) {
            mContrasts.put(mCurrentUser, contrast);
            return true;
        }
        return false;
    }

    private static class ProjectionHolder implements IBinder.DeathRecipient {
        private final String mPackageName;
        private final @UiModeManager.ProjectionType int mProjectionType;