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

Commit f70831e7 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Introduce HalVibratorManager implementation" into main

parents be2bb295 48d75b14
Loading
Loading
Loading
Loading
+346 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 android.hardware.vibrator.IVibrationSession;
import android.hardware.vibrator.IVibratorCallback;
import android.hardware.vibrator.IVibratorManager;
import android.hardware.vibrator.VibrationSessionConfig;
import android.os.Binder;
import android.os.DeadObjectException;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.vibrator.Flags;
import android.util.IndentingPrintWriter;
import android.util.LongSparseArray;
import android.util.Slog;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.internal.annotations.GuardedBy;
import com.android.server.vibrator.VintfUtils.VintfSupplier;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/** Implementations for {@link HalVibratorManager} backed by VINTF objects. */
class VintfHalVibratorManager {

    /** {@link VintfSupplier} for default {@link IVibratorManager} service. */
    static final class DefaultVibratorManagerSupplier extends VintfSupplier<IVibratorManager> {
        @Nullable
        @Override
        IBinder connectToService() {
            return Binder.allowBlocking(ServiceManager.waitForDeclaredService(
                    IVibratorManager.DESCRIPTOR + "/default"));
        }

        @NonNull
        @Override
        IVibratorManager castService(@NonNull IBinder binder) {
            return IVibratorManager.Stub.asInterface(binder);
        }
    }

    /** Default implementation for devices with {@link IVibratorManager} available. */
    static final class DefaultHalVibratorManager implements HalVibratorManager {
        private static final String TAG = "DefaultHalVibratorManager";

        private final Object mLock = new Object();
        @GuardedBy("mLock")
        private final LongSparseArray<IVibrationSession> mOngoingSessions = new LongSparseArray<>();
        private final VintfSupplier<IVibratorManager> mHalSupplier;

        private Callbacks mCallbacks;

        private volatile long mCapabilities = 0;
        private volatile int[] mVibratorIds = new int[0];

        DefaultHalVibratorManager(VintfSupplier<IVibratorManager> supplier) {
            mHalSupplier = supplier;
        }

        @Override
        public void init(@NonNull Callbacks callbacks) {
            mCallbacks = callbacks;

            // Load vibrator hardware info. The vibrator ids and manager capabilities are loaded
            // once and assumed unchanged for the lifecycle of this service. Each vibrator can still
            // retry loading each individual vibrator hardware spec once more at systemReady.
            Optional<Integer> capabilities = VintfUtils.getNoThrow(mHalSupplier,
                    IVibratorManager::getCapabilities,
                    e -> Slog.e(TAG, "Error getting capabilities", e));
            Optional<int[]> vibratorIds = VintfUtils.getNoThrow(mHalSupplier,
                    IVibratorManager::getVibratorIds,
                    e -> Slog.e(TAG, "Error getting vibrator ids", e));
            mCapabilities = capabilities.orElse(0).longValue();
            mVibratorIds = vibratorIds.orElseGet(() -> new int[0]);

            // Reset the hardware to a default state.
            // In case this is a runtime restart instead of a fresh boot.
            cancelSynced();
            if (Flags.vendorVibrationEffects()) {
                clearSessions();
            }
        }

        @Override
        public void onSystemReady() {
        }

        @Override
        public long getCapabilities() {
            return mCapabilities;
        }

        @NonNull
        @Override
        public int[] getVibratorIds() {
            return mVibratorIds;
        }

        @Override
        public boolean prepareSynced(@NonNull int[] vibratorIds) {
            if (!hasCapability(IVibratorManager.CAP_SYNC)) {
                Slog.w(TAG, "No capability to synchronize vibrations, ignoring prepare request.");
                return false;
            }
            return VintfUtils.runNoThrow(mHalSupplier,
                    hal -> hal.prepareSynced(vibratorIds),
                    e -> Slog.e(TAG, "Error preparing synced vibration on vibrator ids: "
                            + Arrays.toString(vibratorIds), e));
        }

        @Override
        public boolean triggerSynced(long vibrationId) {
            if (!hasCapability(IVibratorManager.CAP_SYNC)) {
                Slog.w(TAG, "No capability to synchronize vibrations, ignoring trigger request.");
                return false;
            }
            final IVibratorCallback callback =
                    hasCapability(IVibratorManager.CAP_TRIGGER_CALLBACK)
                            ? new SyncedVibrationCallback(this, vibrationId)
                            : null;
            return VintfUtils.runNoThrow(mHalSupplier,
                    hal -> hal.triggerSynced(callback),
                    e -> Slog.e(TAG, "Error triggering synced vibration " + vibrationId, e));
        }

        @Override
        public boolean cancelSynced() {
            if (!hasCapability(IVibratorManager.CAP_SYNC)) {
                Slog.w(TAG, "No capability to synchronize vibrations, ignoring cancel request.");
                return false;
            }
            return VintfUtils.runNoThrow(mHalSupplier,
                    IVibratorManager::cancelSynced,
                    e -> Slog.e(TAG, "Error canceling synced vibration", e));
        }

        @Override
        public boolean startSession(long sessionId, @NonNull int[] vibratorIds) {
            if (!hasCapability(IVibratorManager.CAP_START_SESSIONS)) {
                Slog.w(TAG, "No capability to start sessions, ignoring start session request.");
                return false;
            }
            final IVibratorCallback callback = new SessionCallback(this, sessionId);
            VibrationSessionConfig config = new VibrationSessionConfig();
            Optional<IVibrationSession> session = VintfUtils.getNoThrow(mHalSupplier,
                    hal -> hal.startSession(vibratorIds, config, callback),
                    e -> Slog.e(TAG, "Error starting vibration session " + sessionId
                            + " on vibrators " + Arrays.toString(vibratorIds), e));
            if (session.isPresent()) {
                synchronized (mLock) {
                    mOngoingSessions.put(sessionId, session.get());
                }
                return true;
            }
            return false;
        }

        @Override
        public boolean endSession(long sessionId, boolean shouldAbort) {
            if (!hasCapability(IVibratorManager.CAP_START_SESSIONS)) {
                Slog.w(TAG, "No capability to start sessions, ignoring end session request.");
                return false;
            }
            IVibrationSession session;
            synchronized (mLock) {
                session = mOngoingSessions.get(sessionId);
            }
            if (session == null) {
                Slog.w(TAG, "No session with id " + sessionId + " to end, ignoring request.");
                return false;
            }
            try {
                if (shouldAbort) {
                    session.abort();
                } else {
                    session.close();
                }
                return true;
            } catch (RemoteException | RuntimeException e) {
                if (e instanceof DeadObjectException) {
                    removeSession(sessionId);
                }
                Slog.e(TAG, "Error ending session " + sessionId + " with abort=" + shouldAbort, e);
            }
            return false;
        }

        @Override
        public void dump(IndentingPrintWriter pw) {
            pw.println("Default Hal VibratorManager:");
            pw.increaseIndent();
            pw.println("capabilities = " + Arrays.toString(getCapabilitiesNames()));
            pw.println("capabilitiesFlags = " + Long.toBinaryString(mCapabilities));
            pw.println("vibratorIds = " + Arrays.toString(mVibratorIds));
            pw.println("ongoingSessionsCount = " + mOngoingSessions.size());
            pw.decreaseIndent();
        }

        @Override
        public String toString() {
            return "DefaultHalVibratorManager{"
                    + ", mCapabilities=" + Arrays.toString(getCapabilitiesNames())
                    + ", mCapabilities flags=" + Long.toBinaryString(mCapabilities)
                    + ", mVibratorIds=" + Arrays.toString(mVibratorIds)
                    + ", mOngoingSessions count=" + mOngoingSessions.size()
                    + '}';
        }

        private void clearSessions() {
            if (!hasCapability(IVibratorManager.CAP_START_SESSIONS)) {
                Slog.w(TAG, "No capability to start sessions, ignoring clear sessions request.");
                return;
            }
            VintfUtils.runNoThrow(mHalSupplier,
                    IVibratorManager::clearSessions,
                    e -> Slog.e(TAG, "Error clearing vibration sessions", e));
        }

        private void removeSession(long sessionId) {
            synchronized (mLock) {
                mOngoingSessions.remove(sessionId);
            }
        }

        private String[] getCapabilitiesNames() {
            List<String> names = new ArrayList<>();
            if (hasCapability(IVibratorManager.CAP_SYNC)) {
                names.add("SYNC");
            }
            if (hasCapability(IVibratorManager.CAP_PREPARE_ON)) {
                names.add("PREPARE_ON");
            }
            if (hasCapability(IVibratorManager.CAP_PREPARE_PERFORM)) {
                names.add("PREPARE_PERFORM");
            }
            if (hasCapability(IVibratorManager.CAP_PREPARE_COMPOSE)) {
                names.add("PREPARE_COMPOSE");
            }
            if (hasCapability(IVibratorManager.CAP_TRIGGER_CALLBACK)) {
                names.add("TRIGGER_CALLBACK");
            }
            if (hasCapability(IVibratorManager.CAP_MIXED_TRIGGER_ON)) {
                names.add("MIXED_TRIGGER_ON");
            }
            if (hasCapability(IVibratorManager.CAP_MIXED_TRIGGER_PERFORM)) {
                names.add("MIXED_TRIGGER_PERFORM");
            }
            if (hasCapability(IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE)) {
                names.add("MIXED_TRIGGER_COMPOSE");
            }
            if (hasCapability(IVibratorManager.CAP_START_SESSIONS)) {
                names.add("START_SESSIONS");
            }
            return names.toArray(new String[names.size()]);
        }

        /** Provides {@link IVibratorCallback} without references to local instances. */
        private static final class SyncedVibrationCallback extends IVibratorCallback.Stub {
            private final WeakReference<DefaultHalVibratorManager> mManagerRef;
            private final long mVibrationId;

            SyncedVibrationCallback(DefaultHalVibratorManager manager, long vibrationId) {
                mManagerRef = new WeakReference<>(manager);
                mVibrationId = vibrationId;
            }

            @Override
            public void onComplete() {
                DefaultHalVibratorManager manager = mManagerRef.get();
                if (manager == null) {
                    return;
                }
                Callbacks callbacks = manager.mCallbacks;
                if (callbacks != null) {
                    callbacks.onSyncedVibrationComplete(mVibrationId);
                }
            }

            @Override
            public int getInterfaceVersion() {
                return IVibratorCallback.VERSION;
            }

            @Override
            public String getInterfaceHash() {
                return IVibratorCallback.HASH;
            }
        }

        /** Provides {@link IVibratorCallback} without references to local instances. */
        private static final class SessionCallback extends IVibratorCallback.Stub {
            private final WeakReference<DefaultHalVibratorManager> mManagerRef;
            private final long mSessionId;

            SessionCallback(DefaultHalVibratorManager manager, long sessionId) {
                mManagerRef = new WeakReference<>(manager);
                mSessionId = sessionId;
            }

            @Override
            public void onComplete() {
                DefaultHalVibratorManager manager = mManagerRef.get();
                if (manager == null) {
                    return;
                }
                manager.removeSession(mSessionId);
                Callbacks callbacks = manager.mCallbacks;
                if (callbacks != null) {
                    callbacks.onVibrationSessionComplete(mSessionId);
                }
            }

            @Override
            public int getInterfaceVersion() {
                return IVibratorCallback.VERSION;
            }

            @Override
            public String getInterfaceHash() {
                return IVibratorCallback.HASH;
            }
        }
    }
}
+139 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.DeadObjectException;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;

import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;

/** Helpers for interacting with VINTF objects. */
class VintfUtils {

    /** {@link Supplier} that takes a VINTF object and throw {@link RemoteException} */
    @FunctionalInterface
    interface VintfGetter<I, R> {
        @Nullable
        R get(I hal) throws RemoteException;
    }

    /** {@link Runnable} that takes a VINTF object and throw {@link RemoteException} */
    @FunctionalInterface
    interface VintfRunnable<I> {
        void run(I hal) throws RemoteException;
    }

    /** Cached {@link Supplier} for remote VINTF objects that resets on dead object. */
    abstract static class VintfSupplier<I> implements Supplier<I>, IBinder.DeathRecipient {
        private static final String TAG = "VintfSupplier";

        @GuardedBy("this")
        private I mInstance = null;

        @Nullable
        abstract IBinder connectToService();

        @NonNull
        abstract I castService(@NonNull IBinder binder);

        @Nullable
        @Override
        public synchronized I get() {
            if (mInstance == null) {
                IBinder binder = connectToService();
                if (binder != null) {
                    mInstance = castService(binder);
                    try {
                        binder.linkToDeath(this, 0);
                    } catch (RemoteException e) {
                        Slog.e(TAG, "Unable to register DeathRecipient for " + mInstance);
                    }
                }
            }
            return mInstance;
        }

        @Override
        public synchronized void binderDied() {
            clear();
        }

        public synchronized void clear() {
            mInstance = null;
        }
    }

    /**
     * Runs getter on VINTF object provided by supplier, if any.
     *
     * <p>This automatically clears the cached object in given {@code supplier} if a
     * {@link DeadObjectException} is thrown by the remote method call, so future interactions can
     * load a new instance.
     *
     * @throws RuntimeException if supplier returns null or there is a {@link RemoteException} or
     * {@link RuntimeException} from the remote method call.
     */
    @Nullable
    static <I, T> T get(VintfSupplier<I> supplier, VintfGetter<I, T> getter) {
        I hal = supplier.get();
        if (hal == null) {
            throw new RuntimeException("Missing HAL service");
        }
        try {
            return getter.get(hal);
        } catch (RemoteException e) {
            if (e instanceof DeadObjectException) {
                supplier.clear();
            }
            throw e.rethrowAsRuntimeException();
        }
    }

    /** Same as {@link #get(VintfSupplier, VintfGetter)}, but throws no exception. */
    @NonNull
    static <I, T> Optional<T> getNoThrow(VintfSupplier<I> supplier, VintfGetter<I, T> getter,
            Consumer<Throwable> errorHandler) {
        try {
            return Optional.ofNullable(get(supplier, getter));
        } catch (RuntimeException e) {
            errorHandler.accept(e);
        }
        return Optional.empty();
    }

    /** Same as {@link #getNoThrow}, but returns {@code true} when successful. */
    static <I> boolean runNoThrow(VintfSupplier<I> supplier, VintfRunnable<I> runnable,
            Consumer<Throwable> errorHandler) {
        VintfGetter<I, Boolean> getter = hal -> {
            runnable.run(hal);
            return true;
        };
        return getNoThrow(supplier, getter, errorHandler).orElse(false);
    }

    // Non-instantiable helper class.
    private VintfUtils() {
    }
}
+28 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 com.android.server.vibrator.VintfHalVibratorManager.DefaultHalVibratorManager;

/** Test class for {@link DefaultHalVibratorManager}. */
public class DefaultHalVibratorManagerTest extends HalVibratorManagerTestCase {

    @Override
    HalVibratorManager newVibratorManager() {
        return mHelper.newDefaultVibratorManager();
    }
}
+68 −3
Original line number Diff line number Diff line
@@ -18,6 +18,9 @@ package com.android.server.vibrator;

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

import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;

import android.hardware.vibrator.IVibratorManager;
import android.os.test.TestLooper;
import android.os.vibrator.Flags;
@@ -64,7 +67,7 @@ public abstract class HalVibratorManagerTestCase {
    @Test
    @EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
    public void init_initializesHalAndClearSyncedAndSessions() {
        mHelper.setCapabilities(IVibratorManager.CAP_SYNC);
        mHelper.setCapabilities(IVibratorManager.CAP_SYNC | IVibratorManager.CAP_START_SESSIONS);
        mHelper.setVibratorIds(new int[] {1, 2});
        HalVibratorManager manager = newVibratorManager();
        manager.init(mHalCallbackMock);
@@ -165,6 +168,21 @@ public abstract class HalVibratorManagerTestCase {
        assertThat(manager.triggerSynced(/* vibrationId= */ 1)).isFalse();
    }

    @Test
    public void triggerSynced_triggerCallback_returnsVibrationId() {
        mHelper.setCapabilities(IVibratorManager.CAP_SYNC | IVibratorManager.CAP_TRIGGER_CALLBACK);
        mHelper.setVibratorIds(new int[] {1, 2});
        HalVibratorManager manager = newInitializedVibratorManager();

        long vibrationId = 1;
        assertThat(manager.triggerSynced(vibrationId)).isTrue();

        mHelper.endLastSyncedVibration();
        mTestLooper.dispatchAll();

        verify(mHalCallbackMock).onSyncedVibrationComplete(eq(vibrationId));
    }

    @Test
    public void cancelSynced_withCapability_returnsTrue() {
        mHelper.setCapabilities(IVibratorManager.CAP_SYNC);
@@ -230,8 +248,10 @@ public abstract class HalVibratorManagerTestCase {
        mHelper.setVibratorIds(new int[] {1, 2, 3});
        HalVibratorManager manager = newInitializedVibratorManager();

        assertThat(manager.endSession(/* sessionId= */ 1, /* shouldAbort= */ true)).isTrue();
        assertThat(manager.endSession(/* sessionId= */ 2, /* shouldAbort= */ false)).isTrue();
        long sessionId = 1;
        assertThat(manager.startSession(sessionId, new int[] { 1 })).isTrue();
        assertThat(manager.endSession(sessionId, /* shouldAbort= */ false)).isTrue();
        assertThat(manager.endSession(sessionId, /* shouldAbort= */ true)).isTrue();
    }

    @Test
@@ -242,4 +262,49 @@ public abstract class HalVibratorManagerTestCase {
        assertThat(manager.endSession(/* sessionId= */ 1, /* shouldAbort= */ true)).isFalse();
        assertThat(manager.endSession(/* sessionId= */ 2, /* shouldAbort= */ false)).isFalse();
    }

    @Test
    public void endSession_returnsSessionId() {
        mHelper.setCapabilities(IVibratorManager.CAP_START_SESSIONS);
        mHelper.setVibratorIds(new int[] {1, 2});
        HalVibratorManager manager = newInitializedVibratorManager();

        long sessionId = 1;
        assertThat(manager.startSession(sessionId, new int[] { 1 })).isTrue();

        manager.endSession(sessionId, /* shouldAbort= */ false);
        mTestLooper.dispatchAll();

        verify(mHalCallbackMock).onVibrationSessionComplete(eq(sessionId));
    }

    @Test
    public void abortSession_returnsSessionId() {
        mHelper.setCapabilities(IVibratorManager.CAP_START_SESSIONS);
        mHelper.setVibratorIds(new int[] {1, 2});
        HalVibratorManager manager = newInitializedVibratorManager();

        long sessionId = 1;
        assertThat(manager.startSession(sessionId, new int[] { 1 })).isTrue();

        manager.endSession(sessionId, /* shouldAbort= */ true);
        mTestLooper.dispatchAll();

        verify(mHalCallbackMock).onVibrationSessionComplete(eq(sessionId));
    }

    @Test
    public void endSessionFromHal_returnsSessionId() {
        mHelper.setCapabilities(IVibratorManager.CAP_START_SESSIONS);
        mHelper.setVibratorIds(new int[] {1, 2});
        HalVibratorManager manager = newInitializedVibratorManager();

        long sessionId = 1;
        assertThat(manager.startSession(sessionId, new int[] { 1 })).isTrue();

        mHelper.endLastSessionAbruptly();
        mTestLooper.dispatchAll();

        verify(mHalCallbackMock).onVibrationSessionComplete(eq(sessionId));
    }
}
+17 −14

File changed.

Preview size limit exceeded, changes collapsed.

Loading