/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.companion.virtual;

import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ACTIVITY;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;

import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.UserIdInt;
import android.app.PendingIntent;
import android.companion.virtual.audio.VirtualAudioDevice;
import android.companion.virtual.camera.VirtualCamera;
import android.companion.virtual.camera.VirtualCameraConfig;
import android.companion.virtual.sensor.VirtualSensor;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.hardware.display.DisplayManagerGlobal;
import android.hardware.display.IVirtualDisplayCallback;
import android.hardware.display.VirtualDisplay;
import android.hardware.display.VirtualDisplayConfig;
import android.hardware.input.IVirtualDpad;
import android.hardware.input.IVirtualKeyboard;
import android.hardware.input.IVirtualMouse;
import android.hardware.input.IVirtualNavigationTouchpad;
import android.hardware.input.IVirtualRotaryEncoder;
import android.hardware.input.IVirtualStylus;
import android.hardware.input.IVirtualTouchscreen;
import android.hardware.input.VirtualDpad;
import android.hardware.input.VirtualDpadConfig;
import android.hardware.input.VirtualKeyboard;
import android.hardware.input.VirtualKeyboardConfig;
import android.hardware.input.VirtualMouse;
import android.hardware.input.VirtualMouseConfig;
import android.hardware.input.VirtualNavigationTouchpad;
import android.hardware.input.VirtualNavigationTouchpadConfig;
import android.hardware.input.VirtualRotaryEncoder;
import android.hardware.input.VirtualRotaryEncoderConfig;
import android.hardware.input.VirtualStylus;
import android.hardware.input.VirtualStylusConfig;
import android.hardware.input.VirtualTouchscreen;
import android.hardware.input.VirtualTouchscreenConfig;
import android.media.AudioManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.view.WindowManager;

import com.android.internal.annotations.GuardedBy;

import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.IntConsumer;

/**
 * An internal representation of a virtual device.
 *
 * @hide
 */
public class VirtualDeviceInternal {

    private final Context mContext;
    private final IVirtualDevice mVirtualDevice;
    private final Object mActivityListenersLock = new Object();
    @GuardedBy("mActivityListenersLock")
    private final ArrayMap<VirtualDeviceManager.ActivityListener, ActivityListenerDelegate>
            mActivityListeners = new ArrayMap<>();
    private final Object mIntentInterceptorListenersLock = new Object();
    @GuardedBy("mIntentInterceptorListenersLock")
    private final ArrayMap<VirtualDeviceManager.IntentInterceptorCallback,
            IntentInterceptorDelegate> mIntentInterceptorListeners = new ArrayMap<>();
    private final Object mSoundEffectListenersLock = new Object();
    @GuardedBy("mSoundEffectListenersLock")
    private final ArrayMap<VirtualDeviceManager.SoundEffectListener, SoundEffectListenerDelegate>
            mSoundEffectListeners = new ArrayMap<>();
    private final IVirtualDeviceActivityListener mActivityListenerBinder =
            new IVirtualDeviceActivityListener.Stub() {

                @Override
                public void onTopActivityChanged(int displayId, ComponentName topActivity,
                        @UserIdInt int userId) {
                    final long token = Binder.clearCallingIdentity();
                    try {
                        synchronized (mActivityListenersLock) {
                            for (int i = 0; i < mActivityListeners.size(); i++) {
                                mActivityListeners.valueAt(i)
                                        .onTopActivityChanged(displayId, topActivity);
                                mActivityListeners.valueAt(i)
                                        .onTopActivityChanged(displayId, topActivity, userId);
                            }
                        }
                    } finally {
                        Binder.restoreCallingIdentity(token);
                    }
                }

                @Override
                public void onDisplayEmpty(int displayId) {
                    final long token = Binder.clearCallingIdentity();
                    try {
                        synchronized (mActivityListenersLock) {
                            for (int i = 0; i < mActivityListeners.size(); i++) {
                                mActivityListeners.valueAt(i).onDisplayEmpty(displayId);
                            }
                        }
                    } finally {
                        Binder.restoreCallingIdentity(token);
                    }
                }

                @Override
                public void onActivityLaunchBlocked(int displayId, ComponentName componentName,
                        UserHandle user, IntentSender intentSender) {
                    final long token = Binder.clearCallingIdentity();
                    try {
                        synchronized (mActivityListenersLock) {
                            for (int i = 0; i < mActivityListeners.size(); i++) {
                                mActivityListeners.valueAt(i)
                                        .onActivityLaunchBlocked(
                                                displayId, componentName, user, intentSender);
                            }
                        }
                    } finally {
                        Binder.restoreCallingIdentity(token);
                    }
                }

                @Override
                public void onSecureWindowShown(int displayId, ComponentName componentName,
                        UserHandle user) {
                    final long token = Binder.clearCallingIdentity();
                    try {
                        synchronized (mActivityListenersLock) {
                            for (int i = 0; i < mActivityListeners.size(); i++) {
                                mActivityListeners.valueAt(i)
                                        .onSecureWindowShown(displayId, componentName, user);
                            }
                        }
                    } finally {
                        Binder.restoreCallingIdentity(token);
                    }
                }

                @Override
                public void onSecureWindowHidden(int displayId) {
                    final long token = Binder.clearCallingIdentity();
                    try {
                        synchronized (mActivityListenersLock) {
                            for (int i = 0; i < mActivityListeners.size(); i++) {
                                mActivityListeners.valueAt(i).onSecureWindowHidden(displayId);
                            }
                        }
                    } finally {
                        Binder.restoreCallingIdentity(token);
                    }
                }

                @Override
                public void onActivityLaunchRequested(int displayId,
                        @NonNull ComponentName componentName, @UserIdInt int userId) {
                    final long token = Binder.clearCallingIdentity();
                    try {
                        synchronized (mActivityListenersLock) {
                            for (int i = 0; i < mActivityListeners.size(); i++) {
                                mActivityListeners.valueAt(i).onActivityLaunchRequested(displayId,
                                        componentName, userId);
                            }
                        }
                    } finally {
                        Binder.restoreCallingIdentity(token);
                    }
                }
            };

    private final IVirtualDeviceSoundEffectListener mSoundEffectListener =
            new IVirtualDeviceSoundEffectListener.Stub() {
                @Override
                public void onPlaySoundEffect(int soundEffect) {
                    final long token = Binder.clearCallingIdentity();
                    try {
                        synchronized (mSoundEffectListenersLock) {
                            for (int i = 0; i < mSoundEffectListeners.size(); i++) {
                                mSoundEffectListeners.valueAt(i).onPlaySoundEffect(soundEffect);
                            }
                        }
                    } finally {
                        Binder.restoreCallingIdentity(token);
                    }
                }
            };
    @Nullable
    private VirtualAudioDevice mVirtualAudioDevice;

    @RequiresPermission(Manifest.permission.CREATE_VIRTUAL_DEVICE)
    VirtualDeviceInternal(
            IVirtualDeviceManager service,
            Context context,
            int associationId,
            VirtualDeviceParams params) throws RemoteException {
        mContext = context.getApplicationContext();
        mVirtualDevice = service.createVirtualDevice(
                new Binder(),
                mContext.getAttributionSource(),
                associationId,
                params,
                mActivityListenerBinder,
                mSoundEffectListener);
    }

    VirtualDeviceInternal(Context context, IVirtualDevice virtualDevice) {
        mContext = context.getApplicationContext();
        mVirtualDevice = virtualDevice;
        try {
            mVirtualDevice.setListeners(mActivityListenerBinder, mSoundEffectListener);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    int getDeviceId() {
        try {
            return mVirtualDevice.getDeviceId();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @Nullable String getPersistentDeviceId() {
        try {
            return mVirtualDevice.getPersistentDeviceId();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull Context createContext() {
        try {
            return mContext.createDeviceContext(mVirtualDevice.getDeviceId());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    List<VirtualSensor> getVirtualSensorList() {
        try {
            return mVirtualDevice.getVirtualSensorList();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    void goToSleep() {
        try {
            mVirtualDevice.goToSleep();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    void wakeUp() {
        try {
            mVirtualDevice.wakeUp();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    void launchPendingIntent(
            int displayId,
            @NonNull PendingIntent pendingIntent,
            @NonNull Executor executor,
            @NonNull IntConsumer listener) {
        try {
            mVirtualDevice.launchPendingIntent(
                    displayId,
                    pendingIntent,
                    new ResultReceiver(new Handler(Looper.getMainLooper())) {
                        @Override
                        protected void onReceiveResult(int resultCode, Bundle resultData) {
                            super.onReceiveResult(resultCode, resultData);
                            executor.execute(() -> listener.accept(resultCode));
                        }
                    });
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    @Nullable
    VirtualDisplay createVirtualDisplay(
            @NonNull VirtualDisplayConfig config,
            @Nullable @CallbackExecutor Executor executor,
            @Nullable VirtualDisplay.Callback callback) {
        IVirtualDisplayCallback callbackWrapper =
                new DisplayManagerGlobal.VirtualDisplayCallback(callback, executor);
        final int displayId;
        try {
            displayId = mVirtualDevice.createVirtualDisplay(config, callbackWrapper);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
        DisplayManagerGlobal displayManager = DisplayManagerGlobal.getInstance();
        return displayManager.createVirtualDisplayWrapper(config, callbackWrapper, displayId);
    }

    void close() {
        try {
            // This also takes care of unregistering all virtual sensors.
            mVirtualDevice.close();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
        if (mVirtualAudioDevice != null) {
            mVirtualAudioDevice.close();
            mVirtualAudioDevice = null;
        }
    }

    void setDevicePolicy(@VirtualDeviceParams.DynamicPolicyType int policyType,
            @VirtualDeviceParams.DevicePolicy int devicePolicy) {
        switch (policyType) {
            case POLICY_TYPE_RECENTS:
            case POLICY_TYPE_CLIPBOARD:
            case POLICY_TYPE_ACTIVITY:
            case POLICY_TYPE_BLOCKED_ACTIVITY:
                break;
            default:
                throw new IllegalArgumentException("Device policy " + policyType
                        + " cannot be changed at runtime. ");
        }
        try {
            mVirtualDevice.setDevicePolicy(policyType, devicePolicy);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    void addActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
        try {
            mVirtualDevice.addActivityPolicyExemption(exemption);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    void removeActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
        try {
            mVirtualDevice.removeActivityPolicyExemption(exemption);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    void setDevicePolicyForDisplay(int displayId,
            @VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
            @VirtualDeviceParams.DevicePolicy int devicePolicy) {
        switch (policyType) {
            case POLICY_TYPE_RECENTS:
            case POLICY_TYPE_ACTIVITY:
                break;
            default:
                throw new IllegalArgumentException("Device policy " + policyType
                        + " cannot be changed for a specific display. ");
        }

        try {
            mVirtualDevice.setDevicePolicyForDisplay(displayId, policyType, devicePolicy);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    VirtualDpad createVirtualDpad(@NonNull VirtualDpadConfig config) {
        try {
            final IBinder token = new Binder(
                    "android.hardware.input.VirtualDpad:" + config.getInputDeviceName());
            IVirtualDpad device = mVirtualDevice.createVirtualDpad(config, token);
            return new VirtualDpad(config, device);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    VirtualKeyboard createVirtualKeyboard(@NonNull VirtualKeyboardConfig config) {
        try {
            final IBinder token = new Binder(
                    "android.hardware.input.VirtualKeyboard:" + config.getInputDeviceName());
            IVirtualKeyboard device = mVirtualDevice.createVirtualKeyboard(config, token);
            return new VirtualKeyboard(config, device);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    VirtualMouse createVirtualMouse(@NonNull VirtualMouseConfig config) {
        try {
            final IBinder token = new Binder(
                    "android.hardware.input.VirtualMouse:" + config.getInputDeviceName());
            IVirtualMouse device = mVirtualDevice.createVirtualMouse(config, token);
            return new VirtualMouse(config, device);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    VirtualTouchscreen createVirtualTouchscreen(
            @NonNull VirtualTouchscreenConfig config) {
        try {
            final IBinder token = new Binder(
                    "android.hardware.input.VirtualTouchscreen:" + config.getInputDeviceName());
            IVirtualTouchscreen device = mVirtualDevice.createVirtualTouchscreen(config, token);
            return new VirtualTouchscreen(config, device);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    VirtualStylus createVirtualStylus(@NonNull VirtualStylusConfig config) {
        try {
            final IBinder token = new Binder(
                    "android.hardware.input.VirtualStylus:" + config.getInputDeviceName());
            IVirtualStylus device = mVirtualDevice.createVirtualStylus(config, token);
            return new VirtualStylus(config, device);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    VirtualRotaryEncoder createVirtualRotaryEncoder(@NonNull VirtualRotaryEncoderConfig config) {
        try {
            final IBinder token = new Binder(
                    "android.hardware.input.VirtualRotaryEncoder:" + config.getInputDeviceName());
            IVirtualRotaryEncoder device = mVirtualDevice.createVirtualRotaryEncoder(config, token);
            return new VirtualRotaryEncoder(config, device);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    VirtualNavigationTouchpad createVirtualNavigationTouchpad(
            @NonNull VirtualNavigationTouchpadConfig config) {
        try {
            final IBinder token = new Binder(
                    "android.hardware.input.VirtualNavigationTouchpad:"
                            + config.getInputDeviceName());
            IVirtualNavigationTouchpad device = mVirtualDevice.createVirtualNavigationTouchpad(
                    config, token);
            return new VirtualNavigationTouchpad(config, device);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    VirtualAudioDevice createVirtualAudioDevice(
            @NonNull VirtualDisplay display,
            @Nullable Executor executor,
            @Nullable VirtualAudioDevice.AudioConfigurationChangeCallback callback) {
        if (mVirtualAudioDevice == null) {
            try {
                // When using a default policy for audio, the device-aware RECORD_AUDIO permission
                // should not take effect, thus register policies with the default context.
                final Context context =
                        mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO) == DEVICE_POLICY_CUSTOM
                                ? mContext.createDeviceContext(getDeviceId())
                                : mContext;
                mVirtualAudioDevice = new VirtualAudioDevice(context, mVirtualDevice, display,
                        executor, callback, () -> mVirtualAudioDevice = null);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
        return mVirtualAudioDevice;
    }

    @NonNull
    VirtualCamera createVirtualCamera(@NonNull VirtualCameraConfig config) {
        try {
            mVirtualDevice.registerVirtualCamera(config);
            return new VirtualCamera(mVirtualDevice, mVirtualDevice.getVirtualCameraId(config),
                    config);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    void setShowPointerIcon(boolean showPointerIcon) {
        try {
            mVirtualDevice.setShowPointerIcon(showPointerIcon);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    void setDisplayImePolicy(int displayId, @WindowManager.DisplayImePolicy int policy) {
        try {
            mVirtualDevice.setDisplayImePolicy(displayId, policy);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    void setDisplayInTouchMode(int displayId, boolean inTouchMode) {
        try {
            mVirtualDevice.setDisplayInTouchMode(displayId, inTouchMode);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    void setDisplayUiMode(int displayId, int uiMode) {
        try {
            mVirtualDevice.setDisplayUiMode(displayId, uiMode);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    void addActivityListener(
            @CallbackExecutor @NonNull Executor executor,
            @NonNull VirtualDeviceManager.ActivityListener listener) {
        final ActivityListenerDelegate delegate = new ActivityListenerDelegate(
                Objects.requireNonNull(listener), Objects.requireNonNull(executor));
        synchronized (mActivityListenersLock) {
            mActivityListeners.put(listener, delegate);
        }
    }

    void removeActivityListener(@NonNull VirtualDeviceManager.ActivityListener listener) {
        synchronized (mActivityListenersLock) {
            mActivityListeners.remove(Objects.requireNonNull(listener));
        }
    }

    void addSoundEffectListener(@CallbackExecutor @NonNull Executor executor,
            @NonNull VirtualDeviceManager.SoundEffectListener soundEffectListener) {
        final SoundEffectListenerDelegate delegate =
                new SoundEffectListenerDelegate(Objects.requireNonNull(executor),
                        Objects.requireNonNull(soundEffectListener));
        synchronized (mSoundEffectListenersLock) {
            mSoundEffectListeners.put(soundEffectListener, delegate);
        }
    }

    void removeSoundEffectListener(
            @NonNull VirtualDeviceManager.SoundEffectListener soundEffectListener) {
        synchronized (mSoundEffectListenersLock) {
            mSoundEffectListeners.remove(Objects.requireNonNull(soundEffectListener));
        }
    }

    void registerIntentInterceptor(
            @NonNull IntentFilter interceptorFilter,
            @CallbackExecutor @NonNull Executor executor,
            @NonNull VirtualDeviceManager.IntentInterceptorCallback interceptorCallback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(interceptorFilter);
        Objects.requireNonNull(interceptorCallback);
        final IntentInterceptorDelegate delegate =
                new IntentInterceptorDelegate(executor, interceptorCallback);
        try {
            mVirtualDevice.registerIntentInterceptor(delegate, interceptorFilter);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
        synchronized (mIntentInterceptorListenersLock) {
            mIntentInterceptorListeners.put(interceptorCallback, delegate);
        }
    }

    void unregisterIntentInterceptor(
            @NonNull VirtualDeviceManager.IntentInterceptorCallback interceptorCallback) {
        Objects.requireNonNull(interceptorCallback);
        final IntentInterceptorDelegate delegate;
        synchronized (mIntentInterceptorListenersLock) {
            delegate = mIntentInterceptorListeners.remove(interceptorCallback);
        }
        if (delegate != null) {
            try {
                mVirtualDevice.unregisterIntentInterceptor(delegate);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
    }

    /**
     * A wrapper for {@link VirtualDeviceManager.ActivityListener} that executes callbacks on the
     * given executor.
     */
    private static class ActivityListenerDelegate implements VirtualDeviceManager.ActivityListener {
        @NonNull private final VirtualDeviceManager.ActivityListener mActivityListener;
        @NonNull private final Executor mExecutor;

        ActivityListenerDelegate(@NonNull VirtualDeviceManager.ActivityListener listener,
                @NonNull Executor executor) {
            mActivityListener = listener;
            mExecutor = executor;
        }

        @Override
        public void onTopActivityChanged(int displayId, @NonNull ComponentName topActivity) {
            mExecutor.execute(() -> mActivityListener.onTopActivityChanged(displayId, topActivity));
        }

        @Override
        public void onTopActivityChanged(int displayId, @NonNull ComponentName topActivity,
                @UserIdInt int userId) {
            mExecutor.execute(() ->
                    mActivityListener.onTopActivityChanged(displayId, topActivity, userId));
        }

        @Override
        public void onDisplayEmpty(int displayId) {
            mExecutor.execute(() -> mActivityListener.onDisplayEmpty(displayId));
        }

        @Override
        public void onActivityLaunchBlocked(int displayId, @NonNull ComponentName componentName,
                @NonNull UserHandle user, IntentSender intentSender) {
            mExecutor.execute(() ->
                    mActivityListener.onActivityLaunchBlocked(
                            displayId, componentName, user, intentSender));
        }

        @Override
        public void onSecureWindowShown(int displayId, @NonNull ComponentName componentName,
                @NonNull UserHandle user) {
            mExecutor.execute(() ->
                    mActivityListener.onSecureWindowShown(displayId, componentName, user));
        }

        @Override
        public void onSecureWindowHidden(int displayId) {
            mExecutor.execute(() -> mActivityListener.onSecureWindowHidden(displayId));
        }

        @Override
        public void onActivityLaunchRequested(int displayId, @NonNull ComponentName componentName,
                @UserIdInt int userId) {
            mExecutor.execute(() -> mActivityListener.onActivityLaunchRequested(displayId,
                    componentName, userId));
        }
    }

    /**
     * A wrapper for {@link VirtualDeviceManager.IntentInterceptorCallback} that executes callbacks
     * on the given executor.
     */
    private static class IntentInterceptorDelegate extends IVirtualDeviceIntentInterceptor.Stub {
        @NonNull private final VirtualDeviceManager.IntentInterceptorCallback
                mIntentInterceptorCallback;
        @NonNull private final Executor mExecutor;

        private IntentInterceptorDelegate(@NonNull Executor executor,
                @NonNull VirtualDeviceManager.IntentInterceptorCallback interceptorCallback) {
            mExecutor = executor;
            mIntentInterceptorCallback = interceptorCallback;
        }

        @Override
        public void onIntentIntercepted(Intent intent) {
            final long token = Binder.clearCallingIdentity();
            try {
                mExecutor.execute(() -> mIntentInterceptorCallback.onIntentIntercepted(intent));
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }
    }

    /**
     * A wrapper for {@link VirtualDeviceManager.SoundEffectListener} that executes callbacks on the
     * given executor.
     */
    private static class SoundEffectListenerDelegate {
        @NonNull private final VirtualDeviceManager.SoundEffectListener mSoundEffectListener;
        @NonNull private final Executor mExecutor;

        private SoundEffectListenerDelegate(@NonNull Executor executor,
                @NonNull VirtualDeviceManager.SoundEffectListener soundEffectCallback) {
            mSoundEffectListener = soundEffectCallback;
            mExecutor = executor;
        }

        public void onPlaySoundEffect(@AudioManager.SystemSoundEffect int effectType) {
            mExecutor.execute(() -> mSoundEffectListener.onPlaySoundEffect(effectType));
        }
    }
}
