/*
 * 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 android.media;

import android.annotation.NonNull;
import android.media.AudioAttributes.AttributeUsage;
import android.media.audiopolicy.AudioMix;
import android.media.audiopolicy.AudioMixingRule;
import android.media.projection.MediaProjection;
import android.os.RemoteException;

import com.android.internal.util.Preconditions;

/**
 * Configuration for capturing audio played by other apps.
 *
 * Only the following audio can be captured:
 *  - usage MUST be {@link AudioAttributes#USAGE_UNKNOWN} or {@link AudioAttributes#USAGE_GAME}
 *    or {@link AudioAttributes#USAGE_MEDIA}. All other usages CAN NOT be captured.
 *  - audio attributes MUST have its ${@link AudioAttributes.Builder#setAllowedCapturePolicy}
 *    to {@link AudioAttributes#ALLOW_CAPTURE_BY_ALL}.
 *  - played by apps that MUST be in the same user profile as the capturing app
 *    (eg work profile can not capture user profile apps and vice-versa).
 *  - played by apps for which the attribute allowAudioPlaybackCapture in their manifest
 *    MUST either be:
 *      * set to "true"
 *      * not set, and their targetSdkVersion MUST be equal or higher to
 *        {@link android.os.Build.VERSION_CODES#Q}.
 *
 * <p>An example for creating a capture configuration for capturing all media playback:
 *
 * <pre>
 *     MediaProjection mediaProjection;
 *     // Retrieve a audio capable projection from the MediaProjectionManager
 *     AudioPlaybackCaptureConfiguration config =
 *              new AudioPlaybackCaptureConfiguration.Builder(mediaProjection)
 *         .addMatchingUsage(AudioAttributes.USAGE_MEDIA)
 *         .build();
 *     AudioRecord record = new AudioRecord.Builder()
 *         .setAudioPlaybackCaptureConfig(config)
 *         .build();
 * </pre>
 *
 * @see MediaProjectionManager#getMediaProjection(int, Intent)
 * @see AudioRecord.Builder#setAudioPlaybackCaptureConfig(AudioPlaybackCaptureConfiguration)
 */
public final class AudioPlaybackCaptureConfiguration {

    private final AudioMixingRule mAudioMixingRule;
    private final MediaProjection mProjection;

    private AudioPlaybackCaptureConfiguration(AudioMixingRule audioMixingRule,
                                              MediaProjection projection) {
        mAudioMixingRule = audioMixingRule;
        mProjection = projection;
    }

    /**
     * @return the {@code MediaProjection} used to build this object.
     * @see {@code Builder.Builder}
     */
    public @NonNull MediaProjection getMediaProjection() {
        return mProjection;
    }


    /**
     * Returns a mix that routes audio back into the app while still playing it from the speakers.
     *
     * @param audioFormat The format in which to capture the audio.
     */
    @NonNull AudioMix createAudioMix(@NonNull AudioFormat audioFormat) {
        return new AudioMix.Builder(mAudioMixingRule)
                .setFormat(audioFormat)
                .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK | AudioMix.ROUTE_FLAG_RENDER)
                .build();
    }

    /** Builder for creating {@link AudioPlaybackCaptureConfiguration} instances. */
    public static final class Builder {

        private static final int MATCH_TYPE_UNSPECIFIED = 0;
        private static final int MATCH_TYPE_INCLUSIVE = 1;
        private static final int MATCH_TYPE_EXCLUSIVE = 2;

        private static final String ERROR_MESSAGE_MISMATCHED_RULES =
                "Inclusive and exclusive usage rules cannot be combined";
        private static final String ERROR_MESSAGE_START_ACTIVITY_FAILED =
                "startActivityForResult failed";
        private static final String ERROR_MESSAGE_NON_AUDIO_PROJECTION =
                "MediaProjection can not project audio";

        private final AudioMixingRule.Builder mAudioMixingRuleBuilder;
        private final MediaProjection mProjection;
        private int mUsageMatchType = MATCH_TYPE_UNSPECIFIED;
        private int mUidMatchType = MATCH_TYPE_UNSPECIFIED;

        /** @param projection A MediaProjection that supports audio projection. */
        public Builder(@NonNull MediaProjection projection) {
            Preconditions.checkNotNull(projection);
            try {
                Preconditions.checkArgument(projection.getProjection().canProjectAudio(),
                                            ERROR_MESSAGE_NON_AUDIO_PROJECTION);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
            mProjection = projection;
            mAudioMixingRuleBuilder = new AudioMixingRule.Builder();
        }

        /**
         * Only capture audio output with the given {@link AudioAttributes}.
         *
         * <p>If called multiple times, will capture audio output that matches any of the given
         * attributes.
         *
         * @throws IllegalStateException if called in conjunction with
         *     {@link #excludeUsage(int)}.
         */
        public @NonNull Builder addMatchingUsage(@AttributeUsage int usage) {
            Preconditions.checkState(
                    mUsageMatchType != MATCH_TYPE_EXCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
            mAudioMixingRuleBuilder.addRule(new AudioAttributes.Builder().setUsage(usage).build(),
                                            AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
            mUsageMatchType = MATCH_TYPE_INCLUSIVE;
            return this;
        }

        /**
         * Only capture audio output by app with the matching {@code uid}.
         *
         * <p>If called multiple times, will capture audio output by apps whose uid is any of the
         * given uids.
         *
         * @throws IllegalStateException if called in conjunction with {@link #excludeUid(int)}.
         */
        public @NonNull Builder addMatchingUid(int uid) {
            Preconditions.checkState(
                    mUidMatchType != MATCH_TYPE_EXCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
            mAudioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_UID, uid);
            mUidMatchType = MATCH_TYPE_INCLUSIVE;
            return this;
        }

        /**
         * Only capture audio output that does not match the given {@link AudioAttributes}.
         *
         * <p>If called multiple times, will capture audio output that does not match any of the
         * given attributes.
         *
         * @throws IllegalStateException if called in conjunction with
         *     {@link #addMatchingUsage(int)}.
         */
        public @NonNull Builder excludeUsage(@AttributeUsage int usage) {
            Preconditions.checkState(
                    mUsageMatchType != MATCH_TYPE_INCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
            mAudioMixingRuleBuilder.excludeRule(new AudioAttributes.Builder()
                                                    .setUsage(usage)
                                                    .build(),
                                                AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
            mUsageMatchType = MATCH_TYPE_EXCLUSIVE;
            return this;
        }

        /**
         * Only capture audio output by apps that do not have the matching {@code uid}.
         *
         * <p>If called multiple times, will capture audio output by apps whose uid is not any of
         * the given uids.
         *
         * @throws IllegalStateException if called in conjunction with {@link #addMatchingUid(int)}.
         */
        public @NonNull Builder excludeUid(int uid) {
            Preconditions.checkState(
                    mUidMatchType != MATCH_TYPE_INCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
            mAudioMixingRuleBuilder.excludeMixRule(AudioMixingRule.RULE_MATCH_UID, uid);
            mUidMatchType = MATCH_TYPE_EXCLUSIVE;
            return this;
        }

        /**
         * Builds the configuration instance.
         *
         * @throws UnsupportedOperationException if the parameters set are incompatible.
         */
        public @NonNull AudioPlaybackCaptureConfiguration build() {
            return new AudioPlaybackCaptureConfiguration(mAudioMixingRuleBuilder.build(),
                                                         mProjection);
        }
    }
}
