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

Commit b42d6cb4 authored by Jiaming Liu's avatar Jiaming Liu
Browse files

Add notifications for the concurrent display mode

Show system notifications when the concurrent display mode is active
or when the concurrent display mode is canceled due to critical thermal
condition.

Summary of the changes:
1. Added DeviceStateNotificationController to manage the device state
   notifications.
2. In DeviceStateProvider, added "reason" to the callback
   onSupportedDeviceStatesChanged to indicate the cause of the supported
   device states change (e.g. change due to thermal condition)
3. In OverrideRequestController, added flags to the onStatusChanged
   callback to indicate the cause of status change (e.g.
   FLAG_THERMAL_CRITICAL means that the change is caused by critical
   thermal condition).
4. In OverrideRequest, added a uid field, which is used to get the app
   name displayed in the notification.

Fix: 266096470, 261127342, 264799106
Test: atest com.android.server.policy.DeviceStateProviderImplTest
com.android.server.devicestate.DeviceStateManagerServiceTest
com.android.server.devicestate.OverrideRequestControllerTest
com.android.server.devicestate.DeviceStateNotificationControllerTest

Change-Id: Ieb590d47dfbfd37e9e732b179a63ed25e080d0f0
parent 6ec1f430
Loading
Loading
Loading
Loading
+21 −0
Original line number Diff line number Diff line
@@ -207,4 +207,25 @@
         be set by OEMs for devices which use eUICCs. -->
    <integer-array name="non_removable_euicc_slots"></integer-array>

    <!-- Device state identifiers and strings for system notifications. The string arrays must have
         the same length and order as the identifier array. -->
    <integer-array name="device_state_notification_state_identifiers">
        <!-- TODO(b/267231269) change to concurrent display identifier when ready -->
        <item>-1</item>
    </integer-array>
    <string-array name="device_state_notification_names">
        <item>@string/concurrent_display_notification_name</item>
    </string-array>
    <string-array name="device_state_notification_active_titles">
        <item>@string/concurrent_display_notification_active_title</item>
    </string-array>
    <string-array name="device_state_notification_active_contents">
        <item>@string/concurrent_display_notification_active_content</item>
    </string-array>
    <string-array name="device_state_notification_thermal_titles">
        <item>@string/concurrent_display_notification_thermal_title</item>
    </string-array>
    <string-array name="device_state_notification_thermal_contents">
        <item>@string/concurrent_display_notification_thermal_content</item>
    </string-array>
</resources>
+13 −0
Original line number Diff line number Diff line
@@ -6234,4 +6234,17 @@ ul.</string>
    <string name="mic_access_on_toast">Microphone is available</string>
    <!-- Toast message that is shown when the user mutes the microphone from the keyboard. [CHAR LIMIT=TOAST] -->
    <string name="mic_access_off_toast">Microphone is blocked</string>

    <!-- Name of concurrent display notifications. [CHAR LIMIT=NONE] -->
    <string name="concurrent_display_notification_name">Dual screen</string>
    <!-- Title of concurrent display active notification. [CHAR LIMIT=NONE] -->
    <string name="concurrent_display_notification_active_title">Dual screen is on</string>
    <!-- Content of concurrent display active notification. [CHAR LIMIT=NONE] -->
    <string name="concurrent_display_notification_active_content"><xliff:g id="app_name" example="Camera app">%1$s</xliff:g> is using both displays to show content</string>
    <!-- Title of concurrent display thermal notification. [CHAR LIMIT=NONE] -->
    <string name="concurrent_display_notification_thermal_title">Device is too warm</string>
    <!-- Content of concurrent display thermal notification. [CHAR LIMIT=NONE] -->
    <string name="concurrent_display_notification_thermal_content">Dual Screen is unavailable because your phone is getting too warm</string>
    <!-- Text of device state notification turn off button. [CHAR LIMIT=NONE] -->
    <string name="device_state_notification_turn_off_button">Turn off</string>
</resources>
+12 −0
Original line number Diff line number Diff line
@@ -4874,6 +4874,18 @@
  <java-symbol type="array" name="config_deviceStatesAvailableForAppRequests" />
  <java-symbol type="array" name="config_serviceStateLocationAllowedPackages" />
  <java-symbol type="integer" name="config_deviceStateRearDisplay"/>
  <java-symbol type="array" name="device_state_notification_state_identifiers"/>
  <java-symbol type="array" name="device_state_notification_names"/>
  <java-symbol type="array" name="device_state_notification_active_titles"/>
  <java-symbol type="array" name="device_state_notification_active_contents"/>
  <java-symbol type="array" name="device_state_notification_thermal_titles"/>
  <java-symbol type="array" name="device_state_notification_thermal_contents"/>
  <java-symbol type="string" name="concurrent_display_notification_name"/>
  <java-symbol type="string" name="concurrent_display_notification_active_title"/>
  <java-symbol type="string" name="concurrent_display_notification_active_content"/>
  <java-symbol type="string" name="concurrent_display_notification_thermal_title"/>
  <java-symbol type="string" name="concurrent_display_notification_thermal_content"/>
  <java-symbol type="string" name="device_state_notification_turn_off_button"/>
  <java-symbol type="bool" name="config_independentLockscreenLiveWallpaper"/>

  <!-- For app language picker -->
+38 −13
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STA
import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS;
import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_BASE_STATE;
import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_EMULATED_STATE;
import static com.android.server.devicestate.OverrideRequestController.FLAG_THERMAL_CRITICAL;
import static com.android.server.devicestate.OverrideRequestController.STATUS_ACTIVE;
import static com.android.server.devicestate.OverrideRequestController.STATUS_CANCELED;
import static com.android.server.devicestate.OverrideRequestController.STATUS_UNKNOWN;
@@ -183,6 +184,9 @@ public final class DeviceStateManagerService extends SystemService {
    ActivityTaskManagerInternal.ScreenObserver mOverrideRequestScreenObserver =
            new OverrideRequestScreenObserver();

    @NonNull
    private final DeviceStateNotificationController mDeviceStateNotificationController;

    public DeviceStateManagerService(@NonNull Context context) {
        this(context, DeviceStatePolicy.Provider
                .fromResources(context.getResources())
@@ -211,6 +215,13 @@ public final class DeviceStateManagerService extends SystemService {
        mDeviceStatePolicy.getDeviceStateProvider().setListener(mDeviceStateProviderListener);
        mBinderService = new BinderService();
        mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
        mDeviceStateNotificationController = new DeviceStateNotificationController(
                context, mHandler,
                () -> {
                    synchronized (mLock) {
                        mActiveOverride.ifPresent(mOverrideRequestController::cancelRequest);
                    }
                });
    }

    @Override
@@ -346,7 +357,8 @@ public final class DeviceStateManagerService extends SystemService {
        return mBinderService;
    }

    private void updateSupportedStates(DeviceState[] supportedDeviceStates) {
    private void updateSupportedStates(DeviceState[] supportedDeviceStates,
            @DeviceStateProvider.SupportedStatesUpdatedReason int reason) {
        synchronized (mLock) {
            final int[] oldStateIdentifiers = getSupportedStateIdentifiersLocked();

@@ -370,7 +382,7 @@ public final class DeviceStateManagerService extends SystemService {
                return;
            }

            mOverrideRequestController.handleNewSupportedStates(newStateIdentifiers);
            mOverrideRequestController.handleNewSupportedStates(newStateIdentifiers, reason);
            updatePendingStateLocked();

            setRearDisplayStateLocked();
@@ -596,7 +608,7 @@ public final class DeviceStateManagerService extends SystemService {

    @GuardedBy("mLock")
    private void onOverrideRequestStatusChangedLocked(@NonNull OverrideRequest request,
            @OverrideRequestController.RequestStatus int status) {
            @OverrideRequestController.RequestStatus int status, int flags) {
        if (request.getRequestType() == OVERRIDE_REQUEST_TYPE_BASE_STATE) {
            switch (status) {
                case STATUS_ACTIVE:
@@ -616,10 +628,19 @@ public final class DeviceStateManagerService extends SystemService {
            switch (status) {
                case STATUS_ACTIVE:
                    mActiveOverride = Optional.of(request);
                    mDeviceStateNotificationController.showStateActiveNotificationIfNeeded(
                            request.getRequestedState(), request.getUid());
                    break;
                case STATUS_CANCELED:
                    if (mActiveOverride.isPresent() && mActiveOverride.get() == request) {
                        mActiveOverride = Optional.empty();
                        mDeviceStateNotificationController.cancelNotification(
                                request.getRequestedState());
                        if ((flags & FLAG_THERMAL_CRITICAL) == FLAG_THERMAL_CRITICAL) {
                            mDeviceStateNotificationController
                                    .showThermalCriticalNotificationIfNeeded(
                                            request.getRequestedState());
                        }
                    }
                    break;
                case STATUS_UNKNOWN: // same as default
@@ -700,7 +721,7 @@ public final class DeviceStateManagerService extends SystemService {
        }
    }

    private void requestStateInternal(int state, int flags, int callingPid,
    private void requestStateInternal(int state, int flags, int callingPid, int callingUid,
            @NonNull IBinder token, boolean hasControlDeviceStatePermission) {
        synchronized (mLock) {
            final ProcessRecord processRecord = mProcessRecords.get(callingPid);
@@ -721,8 +742,8 @@ public final class DeviceStateManagerService extends SystemService {
                        + " is not supported.");
            }

            OverrideRequest request = new OverrideRequest(token, callingPid, state, flags,
                    OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
            OverrideRequest request = new OverrideRequest(token, callingPid, callingUid,
                    state, flags, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);

            // If we don't have the CONTROL_DEVICE_STATE permission, we want to show the overlay
            if (!hasControlDeviceStatePermission && mRearDisplayState != null
@@ -762,7 +783,7 @@ public final class DeviceStateManagerService extends SystemService {
    }

    private void requestBaseStateOverrideInternal(int state, int flags, int callingPid,
            @NonNull IBinder token) {
            int callingUid, @NonNull IBinder token) {
        synchronized (mLock) {
            final Optional<DeviceState> deviceState = getStateLocked(state);
            if (!deviceState.isPresent()) {
@@ -782,8 +803,8 @@ public final class DeviceStateManagerService extends SystemService {
                        + " token: " + token);
            }

            OverrideRequest request = new OverrideRequest(token, callingPid, state, flags,
                    OVERRIDE_REQUEST_TYPE_BASE_STATE);
            OverrideRequest request = new OverrideRequest(token, callingPid, callingUid,
                    state, flags, OVERRIDE_REQUEST_TYPE_BASE_STATE);
            mOverrideRequestController.addBaseStateRequest(request);
        }
    }
@@ -953,11 +974,12 @@ public final class DeviceStateManagerService extends SystemService {
        @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int mCurrentBaseState;

        @Override
        public void onSupportedDeviceStatesChanged(DeviceState[] newDeviceStates) {
        public void onSupportedDeviceStatesChanged(DeviceState[] newDeviceStates,
                @DeviceStateProvider.SupportedStatesUpdatedReason int reason) {
            if (newDeviceStates.length == 0) {
                throw new IllegalArgumentException("Supported device states must not be empty");
            }
            updateSupportedStates(newDeviceStates);
            updateSupportedStates(newDeviceStates, reason);
        }

        @Override
@@ -1085,6 +1107,7 @@ public final class DeviceStateManagerService extends SystemService {
        @Override // Binder call
        public void requestState(IBinder token, int state, int flags) {
            final int callingPid = Binder.getCallingPid();
            final int callingUid = Binder.getCallingUid();
            // Allow top processes to request a device state change
            // If the calling process ID is not the top app, then we check if this process
            // holds a permission to CONTROL_DEVICE_STATE
@@ -1099,7 +1122,8 @@ public final class DeviceStateManagerService extends SystemService {

            final long callingIdentity = Binder.clearCallingIdentity();
            try {
                requestStateInternal(state, flags, callingPid, token, hasControlStatePermission);
                requestStateInternal(state, flags, callingPid, callingUid, token,
                        hasControlStatePermission);
            } finally {
                Binder.restoreCallingIdentity(callingIdentity);
            }
@@ -1124,6 +1148,7 @@ public final class DeviceStateManagerService extends SystemService {
        @Override // Binder call
        public void requestBaseStateOverride(IBinder token, int state, int flags) {
            final int callingPid = Binder.getCallingPid();
            final int callingUid = Binder.getCallingUid();
            getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE,
                    "Permission required to control base state of device.");

@@ -1133,7 +1158,7 @@ public final class DeviceStateManagerService extends SystemService {

            final long callingIdentity = Binder.clearCallingIdentity();
            try {
                requestBaseStateOverrideInternal(state, flags, callingPid, token);
                requestBaseStateOverrideInternal(state, flags, callingPid, callingUid, token);
            } finally {
                Binder.restoreCallingIdentity(callingIdentity);
            }
+293 −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 com.android.server.devicestate;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.hardware.devicestate.DeviceStateManager;
import android.os.Handler;
import android.util.Slog;
import android.util.SparseArray;

import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;

/**
 * Manages the user-visible device state notifications.
 */
class DeviceStateNotificationController extends BroadcastReceiver {
    private static final String TAG = "DeviceStateNotificationController";

    @VisibleForTesting static final String INTENT_ACTION_CANCEL_STATE =
            "com.android.server.devicestate.INTENT_ACTION_CANCEL_STATE";
    @VisibleForTesting static final int NOTIFICATION_ID = 1;
    @VisibleForTesting static final String CHANNEL_ID = "DeviceStateManager";
    @VisibleForTesting static final String NOTIFICATION_TAG = "DeviceStateManager";

    private final Context mContext;
    private final Handler mHandler;
    private final NotificationManager mNotificationManager;
    private final PackageManager mPackageManager;

    // Stores the notification title and content indexed with the device state identifier.
    private final SparseArray<NotificationInfo> mNotificationInfos;

    // The callback when a device state is requested to be canceled.
    private final Runnable mCancelStateRunnable;

    DeviceStateNotificationController(@NonNull Context context, @NonNull Handler handler,
            @NonNull Runnable cancelStateRunnable) {
        this(context, handler, cancelStateRunnable, getNotificationInfos(context),
                context.getPackageManager(), context.getSystemService(NotificationManager.class));
    }

    @VisibleForTesting
    DeviceStateNotificationController(
            @NonNull Context context, @NonNull Handler handler,
            @NonNull Runnable cancelStateRunnable,
            @NonNull SparseArray<NotificationInfo> notificationInfos,
            @NonNull PackageManager packageManager,
            @NonNull NotificationManager notificationManager) {
        mContext = context;
        mHandler = handler;
        mCancelStateRunnable = cancelStateRunnable;
        mNotificationInfos = notificationInfos;
        mPackageManager = packageManager;
        mNotificationManager = notificationManager;
        mContext.registerReceiver(
                this,
                new IntentFilter(INTENT_ACTION_CANCEL_STATE),
                android.Manifest.permission.CONTROL_DEVICE_STATE,
                mHandler,
                Context.RECEIVER_NOT_EXPORTED);
    }

    /**
     * Displays the ongoing notification indicating that the device state is active. Does nothing if
     * the state does not have an active notification.
     *
     * @param state the active device state identifier.
     * @param requestingAppUid the uid of the requesting app used to retrieve the app name.
     */
    void showStateActiveNotificationIfNeeded(int state, int requestingAppUid) {
        NotificationInfo info = mNotificationInfos.get(state);
        if (info == null || !info.hasActiveNotification()) {
            return;
        }
        String requesterApplicationLabel = getApplicationLabel(requestingAppUid);
        if (requesterApplicationLabel != null) {
            showNotification(
                    info.name, info.activeNotificationTitle,
                    String.format(info.activeNotificationContent, requesterApplicationLabel),
                    true /* ongoing */
            );
        } else {
            Slog.e(TAG, "Cannot determine the requesting app name when showing state active "
                    + "notification. uid=" + requestingAppUid + ", state=" + state);
        }
    }

    /**
     * Displays the notification indicating that the device state is canceled due to thermal
     * critical condition. Does nothing if the state does not have a thermal critical notification.
     *
     * @param state the identifier of the device state being canceled.
     */
    void showThermalCriticalNotificationIfNeeded(int state) {
        NotificationInfo info = mNotificationInfos.get(state);
        if (info == null || !info.hasThermalCriticalNotification()) {
            return;
        }
        showNotification(
                info.name, info.thermalCriticalNotificationTitle,
                info.thermalCriticalNotificationContent, false /* ongoing */
        );
    }

    /**
     * Cancels the notification of the corresponding device state.
     *
     * @param state the device state identifier.
     */
    void cancelNotification(int state) {
        if (!mNotificationInfos.contains(state)) {
            return;
        }
        mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
    }

    @Override
    public void onReceive(@NonNull Context context, @Nullable Intent intent) {
        if (intent != null) {
            if (INTENT_ACTION_CANCEL_STATE.equals(intent.getAction())) {
                mCancelStateRunnable.run();
            }
        }
    }

    /**
     * Displays a notification with the specified name, title, and content.
     *
     * @param name the name of the notification.
     * @param title the title of the notification.
     * @param content the content of the notification.
     * @param ongoing if true, display an ongoing (sticky) notification with a turn off button.
     */
    private void showNotification(
            @NonNull String name, @NonNull String title, @NonNull String content, boolean ongoing) {
        final NotificationChannel channel = new NotificationChannel(
                CHANNEL_ID, name, NotificationManager.IMPORTANCE_HIGH);
        final Notification.Builder builder = new Notification.Builder(mContext, CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_lock) // TODO(b/266833171) update icons when available.
                .setContentTitle(title)
                .setContentText(content)
                .setSubText(name)
                .setLocalOnly(true)
                .setOngoing(ongoing)
                .setCategory(Notification.CATEGORY_SYSTEM);

        if (ongoing) {
            final Intent intent = new Intent(INTENT_ACTION_CANCEL_STATE)
                    .setPackage(mContext.getPackageName());
            final PendingIntent pendingIntent = PendingIntent.getBroadcast(
                    mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE);
            final Notification.Action action = new Notification.Action.Builder(
                    null /* icon */,
                    mContext.getString(R.string.device_state_notification_turn_off_button),
                    pendingIntent)
                    .build();
            builder.addAction(action);
        }

        mNotificationManager.createNotificationChannel(channel);
        mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, builder.build());
    }

    /**
     * Loads the resources for the notifications. The device state identifiers and strings are
     * stored in arrays. All the string arrays must have the same length and same order as the
     * identifier array.
     */
    private static SparseArray<NotificationInfo> getNotificationInfos(Context context) {
        final SparseArray<NotificationInfo> notificationInfos = new SparseArray<>();

        final int[] stateIdentifiers =
                context.getResources().getIntArray(
                        R.array.device_state_notification_state_identifiers);
        final String[] names =
                context.getResources().getStringArray(R.array.device_state_notification_names);
        final String[] activeNotificationTitles =
                context.getResources().getStringArray(
                        R.array.device_state_notification_active_titles);
        final String[] activeNotificationContents =
                context.getResources().getStringArray(
                        R.array.device_state_notification_active_contents);
        final String[] thermalCriticalNotificationTitles =
                context.getResources().getStringArray(
                        R.array.device_state_notification_thermal_titles);
        final String[] thermalCriticalNotificationContents =
                context.getResources().getStringArray(
                        R.array.device_state_notification_thermal_contents);

        if (stateIdentifiers.length != names.length
                || stateIdentifiers.length != activeNotificationTitles.length
                || stateIdentifiers.length != activeNotificationContents.length
                || stateIdentifiers.length != thermalCriticalNotificationTitles.length
                || stateIdentifiers.length != thermalCriticalNotificationContents.length
        ) {
            throw new IllegalStateException(
                    "The length of state identifiers and notification texts must match!");
        }

        for (int i = 0; i < stateIdentifiers.length; i++) {
            int identifier = stateIdentifiers[i];
            if (identifier == DeviceStateManager.INVALID_DEVICE_STATE) {
                continue;
            }

            notificationInfos.put(
                    identifier,
                    new NotificationInfo(
                            names[i], activeNotificationTitles[i], activeNotificationContents[i],
                            thermalCriticalNotificationTitles[i],
                            thermalCriticalNotificationContents[i])
            );
        }

        return notificationInfos;
    }

    /**
     * A helper function to get app name (label) using the app uid.
     *
     * @param uid the uid of the app.
     * @return app name (label) if found, or null otherwise.
     */
    @Nullable
    private String getApplicationLabel(int uid) {
        String packageName = mPackageManager.getNameForUid(uid);
        try {
            ApplicationInfo appInfo = mPackageManager.getApplicationInfo(
                    packageName, PackageManager.ApplicationInfoFlags.of(0));
            return appInfo.loadLabel(mPackageManager).toString();
        } catch (PackageManager.NameNotFoundException e) {
            return null;
        }
    }

    /**
     * A data class storing string resources of the notification of a device state.
     */
    @VisibleForTesting
    static class NotificationInfo {
        public final String name;
        public final String activeNotificationTitle;
        public final String activeNotificationContent;
        public final String thermalCriticalNotificationTitle;
        public final String thermalCriticalNotificationContent;

        NotificationInfo(String name, String activeNotificationTitle,
                String activeNotificationContent, String thermalCriticalNotificationTitle,
                String thermalCriticalNotificationContent) {

            this.name = name;
            this.activeNotificationTitle = activeNotificationTitle;
            this.activeNotificationContent = activeNotificationContent;
            this.thermalCriticalNotificationTitle = thermalCriticalNotificationTitle;
            this.thermalCriticalNotificationContent = thermalCriticalNotificationContent;
        }

        boolean hasActiveNotification() {
            return activeNotificationTitle != null && activeNotificationTitle.length() > 0;
        }

        boolean hasThermalCriticalNotification() {
            return thermalCriticalNotificationTitle != null
                    && thermalCriticalNotificationTitle.length() > 0;
        }
    }
}
Loading