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

Commit a68f5112 authored by Chen Bai's avatar Chen Bai
Browse files

haptic: input source customized haptic feedback loading logic

- Implement the loading logic of haptic feedback customized by input
  device. Note: The scope of this CL only covers customization by input
  source, not input device id.
- This CL covers customizations of Rotary and Touch Screen.

Flag: android.os.vibrator.haptic_feedback_input_source_customization_enabled
Test: VibratorManagerServiceTest
Test: HapticFeedbackVibrationProviderTest
Test: HapticFeedbackCustomizationTest
Bug: 354049335
Change-Id: I5d98d14695faed917a3b417969efc5d4ff53019f
parent e0838caa
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -5466,6 +5466,8 @@
  <!-- For HapticFeedbackConstants configurability defined at HapticFeedbackCustomization -->
  <java-symbol type="string" name="config_hapticFeedbackCustomizationFile" />
  <java-symbol type="xml" name="haptic_feedback_customization" />
  <java-symbol type="xml" name="haptic_feedback_customization_source_rotary_encoder" />
  <java-symbol type="xml" name="haptic_feedback_customization_source_touchscreen" />

  <!-- For ActivityManager PSS profiling configurability -->
  <java-symbol type="bool" name="config_am_disablePssProfiling" />
+18 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2024 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.
  -->

<haptic-feedback-constants/>
+18 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2024 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.
  -->

<haptic-feedback-constants/>
+173 −90
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.vibrator;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
@@ -28,7 +29,10 @@ import android.text.TextUtils;
import android.util.Slog;
import android.util.SparseArray;
import android.util.Xml;
import android.view.InputDevice;

import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.XmlUtils;
import com.android.internal.vibrator.persistence.XmlParserException;
import com.android.internal.vibrator.persistence.XmlReader;
@@ -42,6 +46,7 @@ import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Locale;

/**
 * Class that loads custom {@link VibrationEffect} to be performed for each
@@ -92,105 +97,146 @@ final class HapticFeedbackCustomization {
    private static final String ATTRIBUTE_ID = "id";

    /**
     * Parses the haptic feedback vibration customization XML file for the device, and provides a
     * mapping of the customized effect IDs to their respective {@link VibrationEffect}s.
     *
     * <p>This is potentially expensive, so avoid calling repeatedly. One call is enough, and the
     * caller should process the returned mapping (if any) for further queries.
     *
     * @param res {@link Resources} object to be used for reading the device's resources.
     * @return a {@link SparseArray} that maps each customized haptic feedback effect ID to its
     *      respective {@link VibrationEffect}, or {@code null}, if the device has not configured
     *      a file for haptic feedback constants customization.
     * @throws {@link IOException} if an IO error occurs while parsing the customization XML.
     * @throws {@link CustomizationParserException} for any non-IO error that occurs when parsing
     *      the XML, like an invalid XML content or an invalid haptic feedback constant.
     * A {@link SparseArray} that maps each customized haptic feedback effect ID to its
     * respective {@link VibrationEffect}. If this is empty, system's default vibration will be
     * used.
     */
    @Nullable
    static SparseArray<VibrationEffect> loadVibrations(Resources res, VibratorInfo vibratorInfo)
            throws CustomizationParserException, IOException {
        try {
            return loadVibrationsInternal(res, vibratorInfo);
        } catch (VibrationXmlParser.ParseFailedException
                 | XmlParserException
                 | XmlPullParserException e) {
            throw new CustomizationParserException(
                    "Error parsing haptic feedback customization file.", e);
        }
    }
    @NonNull
    private final SparseArray<VibrationEffect> mHapticCustomizations;

    @Nullable
    private static SparseArray<VibrationEffect> loadVibrationsInternal(
            Resources res, VibratorInfo vibratorInfo) throws
                    CustomizationParserException,
                    IOException,
                    XmlParserException,
                    XmlPullParserException {
    /**
     * A {@link SparseArray} similar to {@link mHapticCustomizations} but for rotary input source
     * specific customization.
     */
    @NonNull
    private final SparseArray<VibrationEffect> mHapticCustomizationsForSourceRotary;

    /**
     * A {@link SparseArray} similar to {@link mHapticCustomizations} but for touch screen input
     * source specific customization.
     */
    @NonNull
    private final SparseArray<VibrationEffect> mHapticCustomizationsForSourceTouchScreen;

    HapticFeedbackCustomization(Resources res, VibratorInfo vibratorInfo) {
        if (!Flags.hapticFeedbackVibrationOemCustomizationEnabled()) {
            Slog.d(TAG, "Haptic feedback customization feature is not enabled.");
            return null;
            mHapticCustomizations = new SparseArray<>();
            mHapticCustomizationsForSourceRotary = new SparseArray<>();
            mHapticCustomizationsForSourceTouchScreen = new SparseArray<>();
            return;
        }

        // Old loading path that reads customization from file at dir defined by config.
        TypedXmlPullParser parser = readCustomizationFile(res);
        if (parser == null) {
            // When old loading path doesn't succeed, try loading customization from resources.
            parser = readCustomizationResources(res);
        }
        if (parser == null) {
            Slog.d(TAG, "No loadable haptic feedback customization.");
            return null;
        // Load base customizations.
        SparseArray<VibrationEffect> hapticCustomizations;
        hapticCustomizations = loadCustomizedFeedbackVibrationFromFile(res, vibratorInfo);
        if (hapticCustomizations.size() == 0) {
            // Input source customized haptic feedback was directly added in res. So, no need to old
            // loading path.
            hapticCustomizations = loadCustomizedFeedbackVibrationFromRes(res, vibratorInfo,
                    R.xml.haptic_feedback_customization);
        }
        mHapticCustomizations = hapticCustomizations;

        XmlUtils.beginDocument(parser, TAG_CONSTANTS);
        XmlValidator.checkTagHasNoUnexpectedAttributes(parser);
        int rootDepth = parser.getDepth();

        SparseArray<VibrationEffect> mapping = new SparseArray<>();
        while (XmlReader.readNextTagWithin(parser, rootDepth)) {
            XmlValidator.checkStartTag(parser, TAG_CONSTANT);
            int customizationDepth = parser.getDepth();
        // Load customizations specified by input sources.
        if (android.os.vibrator.Flags.hapticFeedbackInputSourceCustomizationEnabled()) {
            mHapticCustomizationsForSourceRotary =
                    loadCustomizedFeedbackVibrationFromRes(res, vibratorInfo,
                            R.xml.haptic_feedback_customization_source_rotary_encoder);
            mHapticCustomizationsForSourceTouchScreen =
                    loadCustomizedFeedbackVibrationFromRes(res, vibratorInfo,
                            R.xml.haptic_feedback_customization_source_touchscreen);
        } else {
            mHapticCustomizationsForSourceRotary = new SparseArray<>();
            mHapticCustomizationsForSourceTouchScreen = new SparseArray<>();
        }
    }

            // Only attribute in tag is the `id` attribute.
            XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_ID);
            int effectId = XmlReader.readAttributeIntNonNegative(parser, ATTRIBUTE_ID);
            if (mapping.contains(effectId)) {
                throw new CustomizationParserException(
                        "Multiple customizations found for effect " + effectId);
    @VisibleForTesting
    HapticFeedbackCustomization(@NonNull SparseArray<VibrationEffect> hapticCustomizations,
            @NonNull SparseArray<VibrationEffect> hapticCustomizationsForSourceRotary,
            @NonNull SparseArray<VibrationEffect> hapticCustomizationsForSourceTouchScreen) {
        mHapticCustomizations = hapticCustomizations;
        mHapticCustomizationsForSourceRotary = hapticCustomizationsForSourceRotary;
        mHapticCustomizationsForSourceTouchScreen = hapticCustomizationsForSourceTouchScreen;
    }

            // Move the parser one step into the `<constant>` tag.
            XmlValidator.checkParserCondition(
                    XmlReader.readNextTagWithin(parser, customizationDepth),
                    "Unsupported empty customization tag for effect " + effectId);
    @Nullable
    VibrationEffect getEffect(int effectId) {
        return mHapticCustomizations.get(effectId);
    }

            ParsedVibration parsedVibration = VibrationXmlParser.parseElement(
                    parser, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS);
            VibrationEffect effect = parsedVibration.resolve(vibratorInfo);
            if (effect != null) {
                if (effect.getDuration() == Long.MAX_VALUE) {
                    throw new CustomizationParserException(String.format(
                            "Vibration for effect ID %d is repeating, which is not allowed as a"
                            + " haptic feedback: %s", effectId, effect));
    @Nullable
    VibrationEffect getEffect(int effectId, int inputSource) {
        VibrationEffect resultVibration = null;
        if ((InputDevice.SOURCE_ROTARY_ENCODER & inputSource) != 0) {
            resultVibration = mHapticCustomizationsForSourceRotary.get(effectId);
        } else if ((InputDevice.SOURCE_TOUCHSCREEN & inputSource) != 0) {
            resultVibration = mHapticCustomizationsForSourceTouchScreen.get(effectId);
        }
                mapping.put(effectId, effect);
        if (resultVibration == null) {
            resultVibration = mHapticCustomizations.get(effectId);
        }

            XmlReader.readEndTag(parser, TAG_CONSTANT, customizationDepth);
        return resultVibration;
    }

        // Make checks that the XML ends well.
        XmlReader.readEndTag(parser, TAG_CONSTANTS, rootDepth);
        XmlReader.readDocumentEndTag(parser);
    /**
     * Parses the haptic feedback vibration customization XML file for the device whose directory is
     * specified by config. See {@link R.string.config_hapticFeedbackCustomizationFile}.
     *
     * @return Return a mapping of the customized effect IDs to their respective
     * {@link VibrationEffect}s.
     */
    @NonNull
    private static SparseArray<VibrationEffect> loadCustomizedFeedbackVibrationFromFile(
            Resources res, VibratorInfo vibratorInfo) {
        try {
            TypedXmlPullParser parser = readCustomizationFile(res);
            if (parser == null) {
                Slog.d(TAG, "No loadable haptic feedback customization from file.");
                return new SparseArray<>();
            }
            return parseVibrations(parser, vibratorInfo);
        } catch (XmlPullParserException | XmlParserException | IOException e) {
            Slog.e(TAG, "Error parsing haptic feedback customizations from file", e);
            return new SparseArray<>();
        }
    }

        return mapping;
    /**
     * Parses the haptic feedback vibration customization XML resource for the device.
     *
     * @return Return a mapping of the customized effect IDs to their respective
     * {@link VibrationEffect}s.
     */
    @NonNull
    private static SparseArray<VibrationEffect> loadCustomizedFeedbackVibrationFromRes(
            Resources res, VibratorInfo vibratorInfo, int xmlResId) {
        try {
            TypedXmlPullParser parser = readCustomizationResources(res, xmlResId);
            if (parser == null) {
                Slog.d(TAG, "No loadable haptic feedback customization from res.");
                return new SparseArray<>();
            }
            return parseVibrations(parser, vibratorInfo);
        } catch (XmlPullParserException | XmlParserException | IOException e) {
            Slog.e(TAG, "Error parsing haptic feedback customizations from res", e);
            return new SparseArray<>();
        }
    }

    // TODO(b/356412421): deprecate old path related files.
    private static TypedXmlPullParser readCustomizationFile(Resources res)
            throws XmlPullParserException {
        String customizationFile = res.getString(
                com.android.internal.R.string.config_hapticFeedbackCustomizationFile);
        String customizationFile;
        try {
            customizationFile = res.getString(
                    R.string.config_hapticFeedbackCustomizationFile);
        } catch (Resources.NotFoundException e) {
            Slog.e(TAG, "Customization file directory config not found.", e);
            return null;
        }

        if (TextUtils.isEmpty(customizationFile)) {
            return null;
        }
@@ -211,13 +257,14 @@ final class HapticFeedbackCustomization {
        return parser;
    }

    private static TypedXmlPullParser readCustomizationResources(Resources res) {
    @Nullable
    private static TypedXmlPullParser readCustomizationResources(Resources res, int xmlResId) {
        if (!Flags.loadHapticFeedbackVibrationCustomizationFromResources()) {
            return null;
        }
        final XmlResourceParser resParser;
        try {
            resParser = res.getXml(com.android.internal.R.xml.haptic_feedback_customization);
            resParser = res.getXml(xmlResId);
        } catch (Resources.NotFoundException e) {
            Slog.e(TAG, "Haptic customization resource not found.", e);
            return null;
@@ -226,16 +273,52 @@ final class HapticFeedbackCustomization {
        return XmlUtils.makeTyped(resParser);
    }

    /**
     * Represents an error while parsing a haptic feedback customization XML.
     */
    static final class CustomizationParserException extends Exception {
        private CustomizationParserException(String message) {
            super(message);
    @NonNull
    private static SparseArray<VibrationEffect> parseVibrations(TypedXmlPullParser parser,
            VibratorInfo vibratorInfo)
            throws XmlPullParserException, IOException, XmlParserException {
        XmlUtils.beginDocument(parser, TAG_CONSTANTS);
        XmlValidator.checkTagHasNoUnexpectedAttributes(parser);
        int rootDepth = parser.getDepth();

        SparseArray<VibrationEffect> mapping = new SparseArray<>();
        while (XmlReader.readNextTagWithin(parser, rootDepth)) {
            XmlValidator.checkStartTag(parser, TAG_CONSTANT);
            int customizationDepth = parser.getDepth();

            // Only attribute in tag is the `id` attribute.
            XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_ID);
            int effectId = XmlReader.readAttributeIntNonNegative(parser, ATTRIBUTE_ID);
            if (mapping.contains(effectId)) {
                Slog.e(TAG, "Multiple customizations found for effect " + effectId);
                return new SparseArray<>();
            }

            // Move the parser one step into the `<constant>` tag.
            XmlValidator.checkParserCondition(
                    XmlReader.readNextTagWithin(parser, customizationDepth),
                    "Unsupported empty customization tag for effect " + effectId);

            ParsedVibration parsedVibration = VibrationXmlParser.parseElement(
                    parser, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS);
            VibrationEffect effect = parsedVibration.resolve(vibratorInfo);
            if (effect != null) {
                if (effect.getDuration() == Long.MAX_VALUE) {
                    Slog.e(TAG, String.format(Locale.getDefault(),
                            "Vibration for effect ID %d is repeating, which is not allowed as a"
                                    + " haptic feedback: %s", effectId, effect));
                    return new SparseArray<>();
                }
                mapping.put(effectId, effect);
            }

        private CustomizationParserException(String message, Throwable cause) {
            super(message, cause);
            XmlReader.readEndTag(parser, TAG_CONSTANT, customizationDepth);
        }

        // Make checks that the XML ends well.
        XmlReader.readEndTag(parser, TAG_CONSTANTS, rootDepth);
        XmlReader.readDocumentEndTag(parser);

        return mapping;
    }
}
+132 −166

File changed.

Preview size limit exceeded, changes collapsed.

Loading