1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media;
18 
19 import android.annotation.NonNull;
20 import android.media.AudioAttributes.AttributeUsage;
21 import android.media.audiopolicy.AudioMix;
22 import android.media.audiopolicy.AudioMixingRule;
23 import android.media.audiopolicy.AudioMixingRule.AudioMixMatchCriterion;
24 import android.media.projection.MediaProjection;
25 import android.os.RemoteException;
26 
27 import com.android.internal.util.Preconditions;
28 
29 import java.util.function.ToIntFunction;
30 
31 /**
32  * Configuration for capturing audio played by other apps.
33  *
34  *  When capturing audio signals played by other apps (and yours),
35  *  you will only capture a mix of the audio signals played by players
36  *  (such as AudioTrack or MediaPlayer) which present the following characteristics:
37  *  <ul>
38  *  <li> the usage value MUST be {@link AudioAttributes#USAGE_UNKNOWN} or
39  *       {@link AudioAttributes#USAGE_GAME}
40  *       or {@link AudioAttributes#USAGE_MEDIA}. All other usages CAN NOT be captured. </li>
41  *  <li> AND the capture policy set by their app (with {@link AudioManager#setAllowedCapturePolicy})
42  *       or on each player (with {@link AudioAttributes.Builder#setAllowedCapturePolicy}) is
43  *       {@link AudioAttributes#ALLOW_CAPTURE_BY_ALL}, whichever is the most strict. </li>
44  *  <li> AND their app attribute allowAudioPlaybackCapture in their manifest
45  *       MUST either be: <ul>
46  *       <li> set to "true" </li>
47  *       <li> not set, and their {@code targetSdkVersion} MUST be equal to or greater than
48  *            {@link android.os.Build.VERSION_CODES#Q}.
49  *            Ie. Apps that do not target at least Android Q must explicitly opt-in to be captured
50  *            by a MediaProjection. </li></ul>
51  *  <li> AND their apps MUST be in the same user profile as your app
52  *       (eg work profile cannot capture user profile apps and vice-versa). </li>
53  *  </ul>
54  *
55  * <p>An example for creating a capture configuration for capturing all media playback:
56  *
57  * <pre>
58  *     MediaProjection mediaProjection;
59  *     // Retrieve a audio capable projection from the MediaProjectionManager
60  *     AudioPlaybackCaptureConfiguration config =
61  *         new AudioPlaybackCaptureConfiguration.Builder(mediaProjection)
62  *         .addMatchingUsage(AudioAttributes.USAGE_MEDIA)
63  *         .build();
64  *     AudioRecord record = new AudioRecord.Builder()
65  *         .setAudioPlaybackCaptureConfig(config)
66  *         .build();
67  * </pre>
68  *
69  * @see Builder
70  * @see android.media.projection.MediaProjectionManager#getMediaProjection(int, Intent)
71  * @see AudioRecord.Builder#setAudioPlaybackCaptureConfig(AudioPlaybackCaptureConfiguration)
72  */
73 public final class AudioPlaybackCaptureConfiguration {
74 
75     private final AudioMixingRule mAudioMixingRule;
76     private final MediaProjection mProjection;
77 
AudioPlaybackCaptureConfiguration(AudioMixingRule audioMixingRule, MediaProjection projection)78     private AudioPlaybackCaptureConfiguration(AudioMixingRule audioMixingRule,
79                                               MediaProjection projection) {
80         mAudioMixingRule = audioMixingRule;
81         mProjection = projection;
82     }
83 
84     /**
85      * @return the {@code MediaProjection} used to build this object.
86      * @see Builder#Builder(MediaProjection)
87      */
getMediaProjection()88     public @NonNull MediaProjection getMediaProjection() {
89         return mProjection;
90     }
91 
92     /** @return the usages passed to {@link Builder#addMatchingUsage(int)}. */
93     @AttributeUsage
getMatchingUsages()94     public @NonNull int[] getMatchingUsages() {
95         return getIntPredicates(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE,
96                                 criterion -> criterion.getAudioAttributes().getUsage());
97     }
98 
99     /** @return the UIDs passed to {@link Builder#addMatchingUid(int)}. */
getMatchingUids()100     public @NonNull int[] getMatchingUids() {
101         return getIntPredicates(AudioMixingRule.RULE_MATCH_UID,
102                                 criterion -> criterion.getIntProp());
103     }
104 
105     /** @return the usages passed to {@link Builder#excludeUsage(int)}. */
106     @AttributeUsage
getExcludeUsages()107     public @NonNull int[] getExcludeUsages() {
108         return getIntPredicates(AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_USAGE,
109                                 criterion -> criterion.getAudioAttributes().getUsage());
110     }
111 
112     /** @return the UIDs passed to {@link Builder#excludeUid(int)}.  */
getExcludeUids()113     public @NonNull int[] getExcludeUids() {
114         return getIntPredicates(AudioMixingRule.RULE_EXCLUDE_UID,
115                                 criterion -> criterion.getIntProp());
116     }
117 
getIntPredicates(int rule, ToIntFunction<AudioMixMatchCriterion> getPredicate)118     private int[] getIntPredicates(int rule,
119                                    ToIntFunction<AudioMixMatchCriterion> getPredicate) {
120         return mAudioMixingRule.getCriteria().stream()
121             .filter(criterion -> criterion.getRule() == rule)
122             .mapToInt(getPredicate)
123             .toArray();
124     }
125 
126     /**
127      * Returns a mix that routes audio back into the app while still playing it from the speakers.
128      *
129      * @param audioFormat The format in which to capture the audio.
130      */
createAudioMix(@onNull AudioFormat audioFormat)131     @NonNull AudioMix createAudioMix(@NonNull AudioFormat audioFormat) {
132         return new AudioMix.Builder(mAudioMixingRule)
133                 .setFormat(audioFormat)
134                 .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK | AudioMix.ROUTE_FLAG_RENDER)
135                 .build();
136     }
137 
138     /** Builder for creating {@link AudioPlaybackCaptureConfiguration} instances. */
139     public static final class Builder {
140 
141         private static final int MATCH_TYPE_UNSPECIFIED = 0;
142         private static final int MATCH_TYPE_INCLUSIVE = 1;
143         private static final int MATCH_TYPE_EXCLUSIVE = 2;
144 
145         private static final String ERROR_MESSAGE_MISMATCHED_RULES =
146                 "Inclusive and exclusive usage rules cannot be combined";
147         private static final String ERROR_MESSAGE_START_ACTIVITY_FAILED =
148                 "startActivityForResult failed";
149         private static final String ERROR_MESSAGE_NON_AUDIO_PROJECTION =
150                 "MediaProjection can not project audio";
151 
152         private final AudioMixingRule.Builder mAudioMixingRuleBuilder;
153         private final MediaProjection mProjection;
154         private int mUsageMatchType = MATCH_TYPE_UNSPECIFIED;
155         private int mUidMatchType = MATCH_TYPE_UNSPECIFIED;
156 
157         /** @param projection A MediaProjection that supports audio projection. */
Builder(@onNull MediaProjection projection)158         public Builder(@NonNull MediaProjection projection) {
159             Preconditions.checkNotNull(projection);
160             try {
161                 Preconditions.checkArgument(projection.getProjection().canProjectAudio(),
162                                             ERROR_MESSAGE_NON_AUDIO_PROJECTION);
163             } catch (RemoteException e) {
164                 throw e.rethrowFromSystemServer();
165             }
166             mProjection = projection;
167             mAudioMixingRuleBuilder = new AudioMixingRule.Builder();
168         }
169 
170         /**
171          * Only capture audio output with the given {@link AudioAttributes}.
172          *
173          * <p>If called multiple times, will capture audio output that matches any of the given
174          * attributes.
175          *
176          * @throws IllegalStateException if called in conjunction with
177          *     {@link #excludeUsage(int)}.
178          */
addMatchingUsage(@ttributeUsage int usage)179         public @NonNull Builder addMatchingUsage(@AttributeUsage int usage) {
180             Preconditions.checkState(
181                     mUsageMatchType != MATCH_TYPE_EXCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
182             mAudioMixingRuleBuilder.addRule(new AudioAttributes.Builder().setUsage(usage).build(),
183                                             AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
184             mUsageMatchType = MATCH_TYPE_INCLUSIVE;
185             return this;
186         }
187 
188         /**
189          * Only capture audio output by app with the matching {@code uid}.
190          *
191          * <p>If called multiple times, will capture audio output by apps whose uid is any of the
192          * given uids.
193          *
194          * @throws IllegalStateException if called in conjunction with {@link #excludeUid(int)}.
195          */
addMatchingUid(int uid)196         public @NonNull Builder addMatchingUid(int uid) {
197             Preconditions.checkState(
198                     mUidMatchType != MATCH_TYPE_EXCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
199             mAudioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_UID, uid);
200             mUidMatchType = MATCH_TYPE_INCLUSIVE;
201             return this;
202         }
203 
204         /**
205          * Only capture audio output that does not match the given {@link AudioAttributes}.
206          *
207          * <p>If called multiple times, will capture audio output that does not match any of the
208          * given attributes.
209          *
210          * @throws IllegalStateException if called in conjunction with
211          *     {@link #addMatchingUsage(int)}.
212          */
excludeUsage(@ttributeUsage int usage)213         public @NonNull Builder excludeUsage(@AttributeUsage int usage) {
214             Preconditions.checkState(
215                     mUsageMatchType != MATCH_TYPE_INCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
216             mAudioMixingRuleBuilder.excludeRule(new AudioAttributes.Builder()
217                                                     .setUsage(usage)
218                                                     .build(),
219                                                 AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
220             mUsageMatchType = MATCH_TYPE_EXCLUSIVE;
221             return this;
222         }
223 
224         /**
225          * Only capture audio output by apps that do not have the matching {@code uid}.
226          *
227          * <p>If called multiple times, will capture audio output by apps whose uid is not any of
228          * the given uids.
229          *
230          * @throws IllegalStateException if called in conjunction with {@link #addMatchingUid(int)}.
231          */
excludeUid(int uid)232         public @NonNull Builder excludeUid(int uid) {
233             Preconditions.checkState(
234                     mUidMatchType != MATCH_TYPE_INCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
235             mAudioMixingRuleBuilder.excludeMixRule(AudioMixingRule.RULE_MATCH_UID, uid);
236             mUidMatchType = MATCH_TYPE_EXCLUSIVE;
237             return this;
238         }
239 
240         /**
241          * Builds the configuration instance.
242          *
243          * @throws UnsupportedOperationException if the parameters set are incompatible.
244          */
build()245         public @NonNull AudioPlaybackCaptureConfiguration build() {
246             return new AudioPlaybackCaptureConfiguration(mAudioMixingRuleBuilder.build(),
247                                                          mProjection);
248         }
249     }
250 }
251