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

Commit 0903f20e authored by Yeabkal Wubshit's avatar Yeabkal Wubshit Committed by Android (Google) Code Review
Browse files

Merge "Use custom haptic vibrations in HapticFeedbackVibrationProvider" into main

parents d472dc84 57d6f859
Loading
Loading
Loading
Loading
+149 −29
Original line number Diff line number Diff line
@@ -21,9 +21,16 @@ import android.content.res.Resources;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.Slog;
import android.util.SparseArray;
import android.view.HapticFeedbackConstants;

import com.android.internal.annotations.VisibleForTesting;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Set;

/**
 * Provides the {@link VibrationEffect} and {@link VibrationAttributes} for haptic feedback.
@@ -31,6 +38,8 @@ import java.io.PrintWriter;
 * @hide
 */
public final class HapticFeedbackVibrationProvider {
    private static final String TAG = "HapticFeedbackVibrationProvider";

    private static final VibrationAttributes TOUCH_VIBRATION_ATTRIBUTES =
            VibrationAttributes.createForUsage(VibrationAttributes.USAGE_TOUCH);
    private static final VibrationAttributes PHYSICAL_EMULATION_VIBRATION_ATTRIBUTES =
@@ -42,15 +51,41 @@ public final class HapticFeedbackVibrationProvider {
    private final boolean mHapticTextHandleEnabled;
    // Vibrator effect for haptic feedback during boot when safe mode is enabled.
    private final VibrationEffect mSafeModeEnabledVibrationEffect;
    // Haptic feedback vibration customizations specific to the device.
    // If present and valid, a vibration here will be used for an effect.
    // Otherwise, the system's default vibration will be used.
    @Nullable private final SparseArray<VibrationEffect> mHapticCustomizations;

    /** @hide */
    public HapticFeedbackVibrationProvider(Resources res, Vibrator vibrator) {
        this(res, vibrator, loadHapticCustomizations(res));
    }

    /** @hide */
    @VisibleForTesting HapticFeedbackVibrationProvider(
            Resources res,
            Vibrator vibrator,
            @Nullable SparseArray<VibrationEffect> hapticCustomizations) {
        mVibrator = vibrator;
        mHapticTextHandleEnabled = res.getBoolean(
                com.android.internal.R.bool.config_enableHapticTextHandle);

        if (hapticCustomizations != null) {
            // Clean up the customizations to remove vibrations which may not ever be used due to
            // Vibrator properties or other device configurations.
            removeUnsupportedVibrations(hapticCustomizations, vibrator);
            if (hapticCustomizations.size() == 0) {
                hapticCustomizations = null;
            }
        }
        mHapticCustomizations = hapticCustomizations;

        mSafeModeEnabledVibrationEffect =
                VibrationSettings.createEffectFromResource(
                        res, com.android.internal.R.array.config_safeModeEnabledVibePattern);
                effectHasCustomization(HapticFeedbackConstants.SAFE_MODE_ENABLED)
                        ? mHapticCustomizations.get(HapticFeedbackConstants.SAFE_MODE_ENABLED)
                        : VibrationSettings.createEffectFromResource(
                                res,
                                com.android.internal.R.array.config_safeModeEnabledVibePattern);
    }

    /**
@@ -68,7 +103,7 @@ public final class HapticFeedbackVibrationProvider {
            case HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE:
            case HapticFeedbackConstants.SCROLL_TICK:
            case HapticFeedbackConstants.SEGMENT_TICK:
                return VibrationEffect.get(VibrationEffect.EFFECT_TICK);
                return getVibration(effectId, VibrationEffect.EFFECT_TICK);

            case HapticFeedbackConstants.TEXT_HANDLE_MOVE:
                if (!mHapticTextHandleEnabled) {
@@ -77,13 +112,16 @@ public final class HapticFeedbackVibrationProvider {
                // fallthrough
            case HapticFeedbackConstants.CLOCK_TICK:
            case HapticFeedbackConstants.SEGMENT_FREQUENT_TICK:
                return VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK);
                return getVibration(effectId, VibrationEffect.EFFECT_TEXTURE_TICK);

            case HapticFeedbackConstants.KEYBOARD_RELEASE:
            case HapticFeedbackConstants.VIRTUAL_KEY_RELEASE:
            case HapticFeedbackConstants.ENTRY_BUMP:
            case HapticFeedbackConstants.DRAG_CROSSING:
                return VibrationEffect.get(VibrationEffect.EFFECT_TICK, false);
                return getVibration(
                        effectId,
                        VibrationEffect.EFFECT_TICK,
                        /* fallbackForPredefinedEffect= */ false);

            case HapticFeedbackConstants.KEYBOARD_TAP: // == KEYBOARD_PRESS
            case HapticFeedbackConstants.VIRTUAL_KEY:
@@ -93,46 +131,42 @@ public final class HapticFeedbackVibrationProvider {
            case HapticFeedbackConstants.GESTURE_START:
            case HapticFeedbackConstants.SCROLL_ITEM_FOCUS:
            case HapticFeedbackConstants.SCROLL_LIMIT:
                return VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
                return getVibration(effectId, VibrationEffect.EFFECT_CLICK);

            case HapticFeedbackConstants.LONG_PRESS:
            case HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON:
            case HapticFeedbackConstants.DRAG_START:
            case HapticFeedbackConstants.EDGE_SQUEEZE:
                return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
                return getVibration(effectId, VibrationEffect.EFFECT_HEAVY_CLICK);

            case HapticFeedbackConstants.REJECT:
                return VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
                return getVibration(effectId, VibrationEffect.EFFECT_DOUBLE_CLICK);

            case HapticFeedbackConstants.SAFE_MODE_ENABLED:
                return mSafeModeEnabledVibrationEffect;

            case HapticFeedbackConstants.ASSISTANT_BUTTON:
                if (mVibrator.areAllPrimitivesSupported(
                        VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
                        VibrationEffect.Composition.PRIMITIVE_TICK)) {
                    // quiet ramp, short pause, then sharp tick
                    return VibrationEffect.startComposition()
                            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.25f)
                            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f, 50)
                            .compose();
                }
                // fallback for devices without composition support
                return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
                return getAssistantButtonVibration();

            case HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE:
                return getScaledPrimitiveOrElseEffect(
                        VibrationEffect.Composition.PRIMITIVE_TICK, 0.4f,
                return getVibration(
                        effectId,
                        VibrationEffect.Composition.PRIMITIVE_TICK,
                        /* primitiveScale= */ 0.4f,
                        VibrationEffect.EFFECT_TEXTURE_TICK);

            case HapticFeedbackConstants.TOGGLE_ON:
                return getScaledPrimitiveOrElseEffect(
                        VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f,
                return getVibration(
                        effectId,
                        VibrationEffect.Composition.PRIMITIVE_TICK,
                        /* primitiveScale= */ 0.5f,
                        VibrationEffect.EFFECT_TICK);

            case HapticFeedbackConstants.TOGGLE_OFF:
                return getScaledPrimitiveOrElseEffect(
                        VibrationEffect.Composition.PRIMITIVE_LOW_TICK, 0.2f,
                return getVibration(
                        effectId,
                        VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
                        /* primitiveScale= */ 0.2f,
                        VibrationEffect.EFFECT_TEXTURE_TICK);

            case HapticFeedbackConstants.NO_HAPTICS:
@@ -181,14 +215,100 @@ public final class HapticFeedbackVibrationProvider {
        pw.print("mHapticTextHandleEnabled="); pw.println(mHapticTextHandleEnabled);
    }

    private VibrationEffect getScaledPrimitiveOrElseEffect(
            int primitiveId, float scale, int elseEffectId) {
    private VibrationEffect getVibration(int effectId, int predefinedVibrationEffectId) {
        return getVibration(
                effectId, predefinedVibrationEffectId, /* fallbackForPredefinedEffect= */ true);
    }

    /**
     * Returns the customized vibration for {@code hapticFeedbackId}, or
     * {@code predefinedVibrationEffectId} if a customization does not exist for the haptic
     * feedback.
     *
     * <p>If a customization does not exist and the default predefined effect is to be returned,
     * {@code fallbackForPredefinedEffect} will be used to decide whether or not to fallback
     * to a generic pattern if the predefined effect is not hardware supported.
     *
     * @see VibrationEffect#get(int, boolean)
     */
    private VibrationEffect getVibration(
            int hapticFeedbackId,
            int predefinedVibrationEffectId,
            boolean fallbackForPredefinedEffect) {
        if (effectHasCustomization(hapticFeedbackId)) {
            return mHapticCustomizations.get(hapticFeedbackId);
        }
        return VibrationEffect.get(predefinedVibrationEffectId, fallbackForPredefinedEffect);
    }

    /**
     * Returns the customized vibration for {@code hapticFeedbackId}, or some fallback vibration if
     * a customization does not exist for the ID.
     *
     * <p>The fallback will be a primitive composition formed of {@code primitiveId} and
     * {@code primitiveScale}, if the primitive is supported. Otherwise, it will be a predefined
     * vibration of {@code elsePredefinedVibrationEffectId}.
     */
    private VibrationEffect getVibration(
            int hapticFeedbackId,
            int primitiveId,
            float primitiveScale,
            int elsePredefinedVibrationEffectId) {
        if (effectHasCustomization(hapticFeedbackId)) {
            return mHapticCustomizations.get(hapticFeedbackId);
        }
        if (mVibrator.areAllPrimitivesSupported(primitiveId)) {
            return VibrationEffect.startComposition()
                    .addPrimitive(primitiveId, scale)
                    .addPrimitive(primitiveId, primitiveScale)
                    .compose();
        } else {
            return VibrationEffect.get(elseEffectId);
            return VibrationEffect.get(elsePredefinedVibrationEffectId);
        }
    }

    private VibrationEffect getAssistantButtonVibration() {
        if (effectHasCustomization(HapticFeedbackConstants.ASSISTANT_BUTTON)) {
            return mHapticCustomizations.get(HapticFeedbackConstants.ASSISTANT_BUTTON);
        }
        if (mVibrator.areAllPrimitivesSupported(
                VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
                VibrationEffect.Composition.PRIMITIVE_TICK)) {
            // quiet ramp, short pause, then sharp tick
            return VibrationEffect.startComposition()
                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.25f)
                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f, 50)
                    .compose();
        }
        // fallback for devices without composition support
        return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
    }

    private boolean effectHasCustomization(int effectId) {
        return mHapticCustomizations != null && mHapticCustomizations.contains(effectId);
    }

    @Nullable
    private static SparseArray<VibrationEffect> loadHapticCustomizations(Resources res) {
        try {
            return HapticFeedbackCustomization.loadVibrations(res);
        } catch (IOException | HapticFeedbackCustomization.CustomizationParserException e) {
            Slog.e(TAG, "Unable to load haptic customizations.", e);
            return null;
        }
    }

    private static void removeUnsupportedVibrations(
            SparseArray<VibrationEffect> customizations, Vibrator vibrator) {
        Set<Integer> keysToRemove = new HashSet<>();
        for (int i = 0; i < customizations.size(); i++) {
            int key = customizations.keyAt(i);
            if (!vibrator.areVibrationFeaturesSupported(customizations.get(key))) {
                keysToRemove.add(key);
            }
        }

        for (int key : keysToRemove) {
            customizations.remove(key);
        }
    }
}
+261 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.vibrator;

import static android.os.VibrationEffect.Composition.PRIMITIVE_CLICK;
import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK;
import static android.os.VibrationEffect.EFFECT_TEXTURE_TICK;
import static android.os.VibrationEffect.EFFECT_TICK;
import static android.view.HapticFeedbackConstants.CLOCK_TICK;
import static android.view.HapticFeedbackConstants.CONTEXT_CLICK;
import static android.view.HapticFeedbackConstants.SAFE_MODE_ENABLED;
import static android.view.HapticFeedbackConstants.TEXT_HANDLE_MOVE;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.res.Resources;
import android.os.VibrationEffect;
import android.os.test.FakeVibrator;
import android.util.AtomicFile;
import android.util.SparseArray;

import androidx.test.InstrumentationRegistry;

import com.android.internal.R;

import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import java.io.File;
import java.io.FileOutputStream;

public class HapticFeedbackVibrationProviderTest {
    @Rule public MockitoRule rule = MockitoJUnit.rule();

    private static final VibrationEffect PRIMITIVE_TICK_EFFECT =
            VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, 0.2497f).compose();
    private static final VibrationEffect PRIMITIVE_CLICK_EFFECT =
            VibrationEffect.startComposition().addPrimitive(PRIMITIVE_CLICK, 0.3497f).compose();

    private Context mContext = InstrumentationRegistry.getContext();
    private FakeVibrator mVibrator = new FakeVibrator(mContext);

    @Mock private Resources mResourcesMock;

    @Test
    public void testNonExistentCustomization_useDefault() throws Exception {
        // No customization file is set.
        HapticFeedbackVibrationProvider hapticProvider =
                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);

        assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
                .isEqualTo(VibrationEffect.get(EFFECT_TICK));

        // The customization file specifies no customization.
        setupCustomizationFile("<haptic-feedback-constants></haptic-feedback-constants>");
        hapticProvider = new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);

        assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
                .isEqualTo(VibrationEffect.get(EFFECT_TICK));
    }

    @Test
    public void testExceptionParsingCustomizations_useDefault() throws Exception {
        setupCustomizationFile("<bad-xml></bad-xml>");
        HapticFeedbackVibrationProvider hapticProvider =
                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);

        assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
                .isEqualTo(VibrationEffect.get(EFFECT_TICK));
    }

    @Test
    public void testUseValidCustomizedVibration() throws Exception {
        mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
        SparseArray<VibrationEffect> customizations = new SparseArray<>();
        customizations.put(CONTEXT_CLICK, PRIMITIVE_CLICK_EFFECT);

        HapticFeedbackVibrationProvider hapticProvider =
                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);

        // The override for `CONTEXT_CLICK` is used.
        assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
                .isEqualTo(PRIMITIVE_CLICK_EFFECT);
        // `CLOCK_TICK` has no override, so the default vibration is used.
        assertThat(hapticProvider.getVibrationForHapticFeedback(CLOCK_TICK))
                .isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK));
    }

    @Test
    public void testDoNotUseInvalidCustomizedVibration() throws Exception {
        mockVibratorPrimitiveSupport(new int[] {});
        SparseArray<VibrationEffect> customizations = new SparseArray<>();
        customizations.put(CONTEXT_CLICK, PRIMITIVE_CLICK_EFFECT);

        HapticFeedbackVibrationProvider hapticProvider =
                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);

        // The override for `CONTEXT_CLICK` is not used because the vibration is not supported.
        assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
                .isEqualTo(VibrationEffect.get(EFFECT_TICK));
        // `CLOCK_TICK` has no override, so the default vibration is used.
        assertThat(hapticProvider.getVibrationForHapticFeedback(CLOCK_TICK))
                .isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK));
    }

    @Test
    public void testHapticTextDisabled_noVibrationReturnedForTextHandleMove() throws Exception {
        mockHapticTextSupport(false);
        mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
        SparseArray<VibrationEffect> customizations = new SparseArray<>();
        customizations.put(TEXT_HANDLE_MOVE, PRIMITIVE_CLICK_EFFECT);

        // Test with a customization available for `TEXT_HANDLE_MOVE`.
        HapticFeedbackVibrationProvider hapticProvider =
                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);

        assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE)).isNull();

        // Test with no customization available for `TEXT_HANDLE_MOVE`.
        hapticProvider =
                new HapticFeedbackVibrationProvider(
                        mResourcesMock, mVibrator, /* hapticCustomizations= */ null);

        assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE)).isNull();
    }

    @Test
    public void testHapticTextEnabled_vibrationReturnedForTextHandleMove() throws Exception {
        mockHapticTextSupport(true);
        mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
        SparseArray<VibrationEffect> customizations = new SparseArray<>();
        customizations.put(TEXT_HANDLE_MOVE, PRIMITIVE_CLICK_EFFECT);

        // Test with a customization available for `TEXT_HANDLE_MOVE`.
        HapticFeedbackVibrationProvider hapticProvider =
                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);

        assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE))
                .isEqualTo(PRIMITIVE_CLICK_EFFECT);

        // Test with no customization available for `TEXT_HANDLE_MOVE`.
        hapticProvider =
                new HapticFeedbackVibrationProvider(
                        mResourcesMock, mVibrator, /* hapticCustomizations= */ null);

        assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE))
                .isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK));
    }

    @Test
    public void testValidCustomizationPresentForSafeModeEnabled_usedRegardlessOfVibrationResource()
                throws Exception {
        mockSafeModeEnabledVibration(10, 20, 30, 40);
        mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
        SparseArray<VibrationEffect> customizations = new SparseArray<>();
        customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);

        HapticFeedbackVibrationProvider hapticProvider =
                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);

        assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
                .isEqualTo(PRIMITIVE_CLICK_EFFECT);

        mockSafeModeEnabledVibration(null);
        hapticProvider =
                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);

        assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
                .isEqualTo(PRIMITIVE_CLICK_EFFECT);
    }

    @Test
    public void testNoValidCustomizationPresentForSafeModeEnabled_resourceBasedVibrationUsed()
                throws Exception {
        mockSafeModeEnabledVibration(10, 20, 30, 40);
        SparseArray<VibrationEffect> customizations = new SparseArray<>();
        customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);

        // Test with a customization that is not supported by the vibrator.
        HapticFeedbackVibrationProvider hapticProvider =
                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);

        assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
                .isEqualTo(VibrationEffect.createWaveform(new long[] {10, 20, 30, 40}, -1));

        // Test with no customizations.
        hapticProvider =
                new HapticFeedbackVibrationProvider(
                        mResourcesMock, mVibrator, /* hapticCustomizations= */ null);

        assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
                .isEqualTo(VibrationEffect.createWaveform(new long[] {10, 20, 30, 40}, -1));
    }

    @Test
    public void testNoValidCustomizationAndResourcePresentForSafeModeEnabled_noVibrationUsed()
                throws Exception {
        mockSafeModeEnabledVibration(null);
        SparseArray<VibrationEffect> customizations = new SparseArray<>();
        customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);

        // Test with a customization that is not supported by the vibrator.
        HapticFeedbackVibrationProvider hapticProvider =
                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);

        assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)).isNull();

        // Test with no customizations.
        hapticProvider =
                new HapticFeedbackVibrationProvider(
                        mResourcesMock, mVibrator, /* hapticCustomizations= */ null);

        assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)).isNull();
    }

    private void mockVibratorPrimitiveSupport(int... supportedPrimitives) {
        mVibrator = new FakeVibrator(mContext, supportedPrimitives);
    }

    private void mockHapticTextSupport(boolean supported) {
        when(mResourcesMock.getBoolean(R.bool.config_enableHapticTextHandle)).thenReturn(supported);
    }

    private void mockSafeModeEnabledVibration(int... vibrationPattern) {
        when(mResourcesMock.getIntArray(R.array.config_safeModeEnabledVibePattern))
                .thenReturn(vibrationPattern);
    }

    private void setupCustomizationFile(String xml) throws Exception {
        File file = new File(mContext.getCacheDir(), "test.xml");
        file.createNewFile();

        AtomicFile atomicXmlFile = new AtomicFile(file);
        FileOutputStream fos = atomicXmlFile.startWrite();
        fos.write(xml.getBytes());
        atomicXmlFile.finishWrite(fos);

        when(mResourcesMock.getString(R.string.config_hapticFeedbackCustomizationFile))
                .thenReturn(file.getAbsolutePath());
    }
}
+25 −0

File changed.

Preview size limit exceeded, changes collapsed.