/*
 * Copyright (C) 2019 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.soundtrigger_middleware;

import static android.Manifest.permission.SOUNDTRIGGER_DELEGATE_IDENTITY;

import android.annotation.NonNull;
import android.content.Context;
import android.content.PermissionChecker;
import android.media.permission.ClearCallingIdentityContext;
import android.media.permission.Identity;
import android.media.permission.PermissionUtil;
import android.media.permission.SafeCloseable;
import android.media.soundtrigger.ModelParameterRange;
import android.media.soundtrigger.PhraseSoundModel;
import android.media.soundtrigger.RecognitionConfig;
import android.media.soundtrigger.SoundModel;
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
import android.media.soundtrigger_middleware.ISoundTriggerInjection;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.ISoundTriggerModule;
import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
import android.os.IBinder;
import android.os.RemoteException;

import com.android.server.SystemService;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Objects;

/**
 * This is a wrapper around an {@link ISoundTriggerMiddlewareService} implementation, which exposes
 * it as a Binder service.
 * <p>
 * This is intended to facilitate a pattern of decorating the core implementation (business logic)
 * of the interface with every decorator implementing a different aspect of the service, such as
 * validation and logging. This class acts as the top-level decorator, which also adds the binder-
 * related functionality (hence, it extends ISoundTriggerMiddlewareService.Stub as rather than
 * implements ISoundTriggerMiddlewareService), and does the same thing for child interfaces
 * returned.
 * <p>
 * The inner class {@link Lifecycle} acts as both a factory, composing the various aspect-decorators
 * to create a full-featured implementation, as well as as an entry-point for presenting this
 * implementation as a system service.
 * <p>
 * <b>Exposing this service as a System Service:</b><br>
 * Insert this line into {@link com.android.server.SystemServer}:
 * <code><pre>
 * mSystemServiceManager.startService(SoundTriggerMiddlewareService.Lifecycle.class);
 * </pre></code>
 *
 * {@hide}
 */
public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareService.Stub {
    static private final String TAG = "SoundTriggerMiddlewareService";

    private final @NonNull ISoundTriggerMiddlewareInternal mDelegate;
    private final @NonNull Context mContext;
    // Lightweight object used to delegate injection events to the fake STHAL
    private final @NonNull SoundTriggerInjection mInjection;

    /**
     * Constructor for internal use only. Could be exposed for testing purposes in the future.
     * Users should access this class via {@link Lifecycle}.
     */
    private SoundTriggerMiddlewareService(@NonNull ISoundTriggerMiddlewareInternal delegate,
            @NonNull Context context, @NonNull SoundTriggerInjection injection) {
        mDelegate = Objects.requireNonNull(delegate);
        mContext = context;
        mInjection = injection;
    }

    @Override
    public SoundTriggerModuleDescriptor[] listModulesAsOriginator(Identity identity) {
        try (SafeCloseable ignored = establishIdentityDirect(identity)) {
            return mDelegate.listModules();
        }
    }

    @Override
    public SoundTriggerModuleDescriptor[] listModulesAsMiddleman(Identity middlemanIdentity,
            Identity originatorIdentity) {
        try (SafeCloseable ignored = establishIdentityIndirect(middlemanIdentity,
                originatorIdentity)) {
            return mDelegate.listModules();
        }
    }

    @Override
    public ISoundTriggerModule attachAsOriginator(int handle, Identity identity,
            ISoundTriggerCallback callback) {
        try (SafeCloseable ignored = establishIdentityDirect(Objects.requireNonNull(identity))) {
            return new ModuleService(mDelegate.attach(handle, callback, /* isTrusted= */ false));
        }
    }

    @Override
    public ISoundTriggerModule attachAsMiddleman(int handle, Identity middlemanIdentity,
            Identity originatorIdentity, ISoundTriggerCallback callback, boolean isTrusted) {
        try (SafeCloseable ignored = establishIdentityIndirect(
                Objects.requireNonNull(middlemanIdentity),
                Objects.requireNonNull(originatorIdentity))) {
            return new ModuleService(mDelegate.attach(handle, callback, isTrusted));
        }
    }

    @Override
    @android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
    public void attachFakeHalInjection(@NonNull ISoundTriggerInjection injection) {
        PermissionChecker.checkCallingOrSelfPermissionForPreflight(
                mContext, android.Manifest.permission.MANAGE_SOUND_TRIGGER);
        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
            mInjection.registerClient(Objects.requireNonNull(injection));
        }
    }

    @Override
    protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
        if (mDelegate instanceof Dumpable) {
            ((Dumpable) mDelegate).dump(fout);
        }
    }

    private @NonNull
    SafeCloseable establishIdentityIndirect(Identity middlemanIdentity,
            Identity originatorIdentity) {
        return PermissionUtil.establishIdentityIndirect(mContext, SOUNDTRIGGER_DELEGATE_IDENTITY,
                middlemanIdentity, originatorIdentity);
    }

    private @NonNull
    SafeCloseable establishIdentityDirect(Identity originatorIdentity) {
        return PermissionUtil.establishIdentityDirect(originatorIdentity);
    }

    private final static class ModuleService extends ISoundTriggerModule.Stub {
        private final ISoundTriggerModule mDelegate;

        private ModuleService(ISoundTriggerModule delegate) {
            mDelegate = delegate;
        }

        @Override
        public int loadModel(SoundModel model) throws RemoteException {
            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
                return mDelegate.loadModel(model);
            }
        }

        @Override
        public int loadPhraseModel(PhraseSoundModel model) throws RemoteException {
            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
                return mDelegate.loadPhraseModel(model);
            }
        }

        @Override
        public void unloadModel(int modelHandle) throws RemoteException {
            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
                mDelegate.unloadModel(modelHandle);
            }
        }

        @Override
        public IBinder startRecognition(int modelHandle, RecognitionConfig config)
                throws RemoteException {
            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
                return mDelegate.startRecognition(modelHandle, config);
            }
        }

        @Override
        public void stopRecognition(int modelHandle) throws RemoteException {
            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
                mDelegate.stopRecognition(modelHandle);
            }
        }

        @Override
        public void forceRecognitionEvent(int modelHandle) throws RemoteException {
            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
                mDelegate.forceRecognitionEvent(modelHandle);
            }
        }

        @Override
        public void setModelParameter(int modelHandle, int modelParam, int value)
                throws RemoteException {
            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
                mDelegate.setModelParameter(modelHandle, modelParam, value);
            }
        }

        @Override
        public int getModelParameter(int modelHandle, int modelParam) throws RemoteException {
            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
                return mDelegate.getModelParameter(modelHandle, modelParam);
            }
        }

        @Override
        public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam)
                throws RemoteException {
            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
                return mDelegate.queryModelParameterSupport(modelHandle, modelParam);
            }
        }

        @Override
        public void detach() throws RemoteException {
            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
                mDelegate.detach();
            }
        }
    }

    /**
     * Entry-point to this module: exposes the module as a {@link SystemService}.
     */
    public static final class Lifecycle extends SystemService {
        public Lifecycle(Context context) {
            super(context);
        }

        @Override
        public void onStart() {
            final SoundTriggerInjection injection = new SoundTriggerInjection();
            HalFactory[] factories = new HalFactory[]{new DefaultHalFactory(),
                    new FakeHalFactory(injection)};

            publishBinderService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE,
                    new SoundTriggerMiddlewareService(
                            new SoundTriggerMiddlewareLogging(getContext(),
                                new SoundTriggerMiddlewarePermission(
                                        new SoundTriggerMiddlewareValidation(
                                                new SoundTriggerMiddlewareImpl(factories,
                                                        new AudioSessionProviderImpl())),
                                        getContext())), getContext(),
                                        injection));
        }
    }
}
