blob: 453704eea39861b4f8a2adf9fff553d7ace35bf7 [file] [log] [blame]
/*
* 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.audiopolicy.AudioMixingRule.AudioMixMatchCriterion;
import android.media.projection.MediaProjection;
import android.os.RemoteException;
import com.android.internal.util.Preconditions;
import java.util.function.ToIntFunction;
/**
* Configuration for capturing audio played by other apps.
*
* When capturing audio signals played by other apps (and yours),
* you will only capture a mix of the audio signals played by players
* (such as AudioTrack or MediaPlayer) which present the following characteristics:
* <ul>
* <li> the usage value MUST be {@link AudioAttributes#USAGE_UNKNOWN} or
* {@link AudioAttributes#USAGE_GAME}
* or {@link AudioAttributes#USAGE_MEDIA}. All other usages CAN NOT be captured. </li>
* <li> AND the capture policy set by their app (with {@link AudioManager#setAllowedCapturePolicy})
* or on each player (with {@link AudioAttributes.Builder#setAllowedCapturePolicy}) is
* {@link AudioAttributes#ALLOW_CAPTURE_BY_ALL}, whichever is the most strict. </li>
* <li> AND their app attribute allowAudioPlaybackCapture in their manifest
* MUST either be: <ul>
* <li> set to "true" </li>
* <li> not set, and their {@code targetSdkVersion} MUST be equal to or greater than
* {@link android.os.Build.VERSION_CODES#Q}.
* Ie. Apps that do not target at least Android Q must explicitly opt-in to be captured
* by a MediaProjection. </li></ul>
* <li> AND their apps MUST be in the same user profile as your app
* (eg work profile cannot capture user profile apps and vice-versa). </li>
* </ul>
*
* <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 Builder
* @see android.media.projection.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 Builder#Builder(MediaProjection)
*/
public @NonNull MediaProjection getMediaProjection() {
return mProjection;
}
/** @return the usages passed to {@link Builder#addMatchingUsage(int)}. */
@AttributeUsage
public @NonNull int[] getMatchingUsages() {
return getIntPredicates(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE,
criterion -> criterion.getAudioAttributes().getUsage());
}
/** @return the UIDs passed to {@link Builder#addMatchingUid(int)}. */
public @NonNull int[] getMatchingUids() {
return getIntPredicates(AudioMixingRule.RULE_MATCH_UID,
criterion -> criterion.getIntProp());
}
/** @return the usages passed to {@link Builder#excludeUsage(int)}. */
@AttributeUsage
public @NonNull int[] getExcludeUsages() {
return getIntPredicates(AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_USAGE,
criterion -> criterion.getAudioAttributes().getUsage());
}
/** @return the UIDs passed to {@link Builder#excludeUid(int)}. */
public @NonNull int[] getExcludeUids() {
return getIntPredicates(AudioMixingRule.RULE_EXCLUDE_UID,
criterion -> criterion.getIntProp());
}
private int[] getIntPredicates(int rule,
ToIntFunction<AudioMixMatchCriterion> getPredicate) {
return mAudioMixingRule.getCriteria().stream()
.filter(criterion -> criterion.getRule() == rule)
.mapToInt(getPredicate)
.toArray();
}
/**
* 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);
}
}
}