/*
 * Copyright (C) 2020 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.CAPTURE_AUDIO_HOTWORD;
import static android.Manifest.permission.RECORD_AUDIO;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.PermissionChecker;
import android.media.permission.Identity;
import android.media.permission.IdentityContext;
import android.media.permission.PermissionUtil;
import android.media.soundtrigger.ModelParameterRange;
import android.media.soundtrigger.PhraseSoundModel;
import android.media.soundtrigger.RecognitionConfig;
import android.media.soundtrigger.SoundModel;
import android.media.soundtrigger.Status;
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.ISoundTriggerModule;
import android.media.soundtrigger_middleware.PhraseRecognitionEventSys;
import android.media.soundtrigger_middleware.RecognitionEventSys;
import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceSpecificException;

import com.android.server.LocalServices;
import com.android.server.pm.permission.LegacyPermissionManagerInternal;

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

/**
 * This is a decorator of an {@link ISoundTriggerMiddlewareService}, which enforces permissions.
 * <p>
 * Every public method in this class, overriding an interface method, must follow a similar
 * pattern:
 * <code><pre>
 * @Override public T method(S arg) {
 *     // Permission check.
 *     enforcePermissions*(...);
 *     return mDelegate.method(arg);
 * }
 * </pre></code>
 *
 * {@hide}
 */
public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddlewareInternal, Dumpable {
    private static final String TAG = "SoundTriggerMiddlewarePermission";

    private final @NonNull ISoundTriggerMiddlewareInternal mDelegate;
    private final @NonNull Context mContext;

    public SoundTriggerMiddlewarePermission(
            @NonNull ISoundTriggerMiddlewareInternal delegate, @NonNull Context context) {
        mDelegate = delegate;
        mContext = context;
    }

    @Override
    public @NonNull
    SoundTriggerModuleDescriptor[] listModules() {
        Identity identity = getIdentity();
        enforcePermissionForPreflight(mContext, identity, CAPTURE_AUDIO_HOTWORD);
        return mDelegate.listModules();
    }

    @Override
    public @NonNull
    ISoundTriggerModule attach(int handle,
            @NonNull ISoundTriggerCallback callback, boolean isTrusted) {
        Identity identity = getIdentity();
        enforcePermissionsForPreflight(identity);
        ModuleWrapper wrapper = new ModuleWrapper(identity, callback, isTrusted);
        return wrapper.attach(mDelegate.attach(handle, wrapper.getCallbackWrapper(), isTrusted));
    }

    // Override toString() in order to have the delegate's ID in it.
    @Override
    public String toString() {
        return Objects.toString(mDelegate);
    }

    /**
     * Get the identity context, or throws an InternalServerError if it has not been established.
     *
     * @return The identity.
     */
    private static @NonNull
    Identity getIdentity() {
        return IdentityContext.getNonNull();
    }

    /**
     * Throws a {@link SecurityException} if originator permanently doesn't have the given
     * permission,
     * or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
     * originator temporarily doesn't have the right permissions to use this service.
     */
    private void enforcePermissionsForPreflight(@NonNull Identity identity) {
        enforcePermissionForPreflight(mContext, identity, RECORD_AUDIO);
        enforcePermissionForPreflight(mContext, identity, CAPTURE_AUDIO_HOTWORD);
    }

    /**
     * Throws a {@link SecurityException} iff the originator has permission to receive data.
     */
    void enforcePermissionsForDataDelivery(@NonNull Identity identity, @NonNull String reason) {
        enforceSoundTriggerRecordAudioPermissionForDataDelivery(identity, reason);
        enforcePermissionForDataDelivery(mContext, identity, CAPTURE_AUDIO_HOTWORD,
                reason);
    }

    /**
     * Throws a {@link SecurityException} iff the given identity has given permission to receive
     * data.
     *
     * @param context    A {@link Context}, used for permission checks.
     * @param identity   The identity to check.
     * @param permission The identifier of the permission we want to check.
     * @param reason     The reason why we're requesting the permission, for auditing purposes.
     */
    private static void enforcePermissionForDataDelivery(@NonNull Context context,
            @NonNull Identity identity,
            @NonNull String permission, @NonNull String reason) {
        final int status = PermissionUtil.checkPermissionForDataDelivery(context, identity,
                permission, reason);
        if (status != PermissionChecker.PERMISSION_GRANTED) {
            throw new SecurityException(
                    String.format("Failed to obtain permission %s for identity %s", permission,
                            ObjectPrinter.print(identity, 16)));
        }
    }

    private static void enforceSoundTriggerRecordAudioPermissionForDataDelivery(
            @NonNull Identity identity, @NonNull String reason) {
        LegacyPermissionManagerInternal lpmi =
                LocalServices.getService(LegacyPermissionManagerInternal.class);
        final int status = lpmi.checkSoundTriggerRecordAudioPermissionForDataDelivery(identity.uid,
                identity.packageName, identity.attributionTag, reason);
        if (status != PermissionChecker.PERMISSION_GRANTED) {
            throw new SecurityException(
                    String.format("Failed to obtain permission RECORD_AUDIO for identity %s",
                            ObjectPrinter.print(identity, 16)));
        }
    }

    /**
     * Throws a {@link SecurityException} if originator permanently doesn't have the given
     * permission.
     * Soft (temporary) denials are considered OK for preflight purposes.
     *
     * @param context    A {@link Context}, used for permission checks.
     * @param identity   The identity to check.
     * @param permission The identifier of the permission we want to check.
     */
    private static void enforcePermissionForPreflight(@NonNull Context context,
            @NonNull Identity identity, @NonNull String permission) {
        final int status = PermissionUtil.checkPermissionForPreflight(context, identity,
                permission);
        switch (status) {
            case PermissionChecker.PERMISSION_GRANTED:
            case PermissionChecker.PERMISSION_SOFT_DENIED:
                return;
            case PermissionChecker.PERMISSION_HARD_DENIED:
                throw new SecurityException(
                        String.format("Failed to obtain permission %s for identity %s", permission,
                                ObjectPrinter.print(identity, 16)));
            default:
                throw new RuntimeException("Unexpected perimission check result.");
        }
    }


    @Override
    public void dump(PrintWriter pw) {
        if (mDelegate instanceof Dumpable) {
            ((Dumpable) mDelegate).dump(pw);
        }
    }

    /**
     * A wrapper around an {@link ISoundTriggerModule} implementation, to address the same aspects
     * mentioned in {@link SoundTriggerModule} above. This class follows the same conventions.
     */
    private class ModuleWrapper extends ISoundTriggerModule.Stub {
        private ISoundTriggerModule mDelegate;
        private final @NonNull Identity mOriginatorIdentity;
        private final @NonNull CallbackWrapper mCallbackWrapper;
        private final boolean mIsTrusted;

        ModuleWrapper(@NonNull Identity originatorIdentity,
                @NonNull ISoundTriggerCallback callback,
                boolean isTrusted) {
            mOriginatorIdentity = originatorIdentity;
            mCallbackWrapper = new CallbackWrapper(callback);
            mIsTrusted = isTrusted;
        }

        ModuleWrapper attach(@NonNull ISoundTriggerModule delegate) {
            mDelegate = delegate;
            return this;
        }

        ISoundTriggerCallback getCallbackWrapper() {
            return mCallbackWrapper;
        }

        @Override
        public int loadModel(@NonNull SoundModel model) throws RemoteException {
            enforcePermissions();
            return mDelegate.loadModel(model);
        }

        @Override
        public int loadPhraseModel(@NonNull PhraseSoundModel model) throws RemoteException {
            enforcePermissions();
            return mDelegate.loadPhraseModel(model);
        }

        @Override
        public void unloadModel(int modelHandle) throws RemoteException {
            // Unloading a model does not require special permissions. Having a handle to the
            // session is sufficient.
            mDelegate.unloadModel(modelHandle);

        }

        @Override
        public IBinder startRecognition(int modelHandle, @NonNull RecognitionConfig config)
                throws RemoteException {
            enforcePermissions();
            return mDelegate.startRecognition(modelHandle, config);
        }

        @Override
        public void stopRecognition(int modelHandle) throws RemoteException {
            // Stopping a model does not require special permissions. Having a handle to the
            // session is sufficient.
            mDelegate.stopRecognition(modelHandle);
        }

        @Override
        public void forceRecognitionEvent(int modelHandle) throws RemoteException {
            enforcePermissions();
            mDelegate.forceRecognitionEvent(modelHandle);
        }

        @Override
        public void setModelParameter(int modelHandle, int modelParam, int value)
                throws RemoteException {
            enforcePermissions();
            mDelegate.setModelParameter(modelHandle, modelParam, value);
        }

        @Override
        public int getModelParameter(int modelHandle, int modelParam) throws RemoteException {
            enforcePermissions();
            return mDelegate.getModelParameter(modelHandle, modelParam);
        }

        @Override
        @Nullable
        public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam)
                throws RemoteException {
            enforcePermissions();
            return mDelegate.queryModelParameterSupport(modelHandle,
                    modelParam);
        }

        @Override
        public void detach() throws RemoteException {
            // Detaching does not require special permissions. Having a handle to the session is
            // sufficient.
            mDelegate.detach();
        }

        // Override toString() in order to have the delegate's ID in it.
        @Override
        public String toString() {
            return Objects.toString(mDelegate);
        }

        private void enforcePermissions() {
            enforcePermissionsForPreflight(mOriginatorIdentity);
        }

        private class CallbackWrapper implements ISoundTriggerCallback {
            private final ISoundTriggerCallback mDelegate;

            private CallbackWrapper(ISoundTriggerCallback delegate) {
                mDelegate = delegate;
            }

            @Override
            public void onRecognition(int modelHandle, RecognitionEventSys event,
                    int captureSession) throws RemoteException {
                enforcePermissions("Sound trigger recognition.");
                mDelegate.onRecognition(modelHandle, event, captureSession);
            }

            @Override
            public void onPhraseRecognition(int modelHandle, PhraseRecognitionEventSys event,
                    int captureSession) throws RemoteException {
                enforcePermissions("Sound trigger phrase recognition.");
                mDelegate.onPhraseRecognition(modelHandle, event, captureSession);
            }

            @Override
            public void onResourcesAvailable() throws RemoteException {
                mDelegate.onResourcesAvailable();
            }

            @Override
            public void onModelUnloaded(int modelHandle) throws RemoteException {
                mDelegate.onModelUnloaded(modelHandle);
            }

            @Override
            public void onModuleDied() throws RemoteException {
                mDelegate.onModuleDied();
            }

            @Override
            public IBinder asBinder() {
                return mDelegate.asBinder();
            }

            // Override toString() in order to have the delegate's ID in it.
            @Override
            public String toString() {
                return mDelegate.toString();
            }

            private void enforcePermissions(String reason) {
                if (mIsTrusted) {
                    enforcePermissionsForPreflight(mOriginatorIdentity);
                } else {
                    enforcePermissionsForDataDelivery(mOriginatorIdentity, reason);
                }
            }
        }
    }
}
