• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.annotation.Nullable;
21 import android.app.ActivityThread;
22 import android.app.AppOpsManager;
23 import android.content.Context;
24 import android.os.IBinder;
25 import android.os.Parcel;
26 import android.os.Parcelable;
27 import android.os.Process;
28 import android.os.RemoteException;
29 import android.os.ServiceManager;
30 import android.util.Log;
31 
32 import com.android.internal.annotations.GuardedBy;
33 import com.android.internal.app.IAppOpsCallback;
34 import com.android.internal.app.IAppOpsService;
35 
36 import java.lang.ref.WeakReference;
37 import java.util.Objects;
38 
39 /**
40  * Class to encapsulate a number of common player operations:
41  *   - AppOps for OP_PLAY_AUDIO
42  *   - more to come (routing, transport control)
43  * @hide
44  */
45 public abstract class PlayerBase {
46 
47     private static final String TAG = "PlayerBase";
48     /** Debug app ops */
49     private static final boolean DEBUG_APP_OPS = false;
50     private static final boolean DEBUG = DEBUG_APP_OPS || false;
51     private static IAudioService sService; //lazy initialization, use getService()
52 
53     /** if true, only use OP_PLAY_AUDIO monitoring for logging, and rely on muting to happen
54      *  in AudioFlinger */
55     private static final boolean USE_AUDIOFLINGER_MUTING_FOR_OP = true;
56 
57     // parameters of the player that affect AppOps
58     protected AudioAttributes mAttributes;
59 
60     // volumes of the subclass "player volumes", as seen by the client of the subclass
61     //   (e.g. what was passed in AudioTrack.setVolume(float)). The actual volume applied is
62     //   the combination of the player volume, and the PlayerBase pan and volume multipliers
63     protected float mLeftVolume = 1.0f;
64     protected float mRightVolume = 1.0f;
65     protected float mAuxEffectSendLevel = 0.0f;
66 
67     // NEVER call into AudioService (see getService()) with mLock held: PlayerBase can run in
68     // the same process as AudioService, which can synchronously call back into this class,
69     // causing deadlocks between the two
70     private final Object mLock = new Object();
71 
72     // for AppOps
73     private @Nullable IAppOpsService mAppOps;
74     private @Nullable IAppOpsCallback mAppOpsCallback;
75     @GuardedBy("mLock")
76     private boolean mHasAppOpsPlayAudio = true;
77 
78     private final int mImplType;
79     // uniquely identifies the Player Interface throughout the system (P I Id)
80     private int mPlayerIId = AudioPlaybackConfiguration.PLAYER_PIID_INVALID;
81 
82     @GuardedBy("mLock")
83     private int mState;
84     @GuardedBy("mLock")
85     private int mStartDelayMs = 0;
86     @GuardedBy("mLock")
87     private float mPanMultiplierL = 1.0f;
88     @GuardedBy("mLock")
89     private float mPanMultiplierR = 1.0f;
90     @GuardedBy("mLock")
91     private float mVolMultiplier = 1.0f;
92 
93     /**
94      * Constructor. Must be given audio attributes, as they are required for AppOps.
95      * @param attr non-null audio attributes
96      * @param class non-null class of the implementation of this abstract class
97      */
PlayerBase(@onNull AudioAttributes attr, int implType)98     PlayerBase(@NonNull AudioAttributes attr, int implType) {
99         if (attr == null) {
100             throw new IllegalArgumentException("Illegal null AudioAttributes");
101         }
102         mAttributes = attr;
103         mImplType = implType;
104         mState = AudioPlaybackConfiguration.PLAYER_STATE_IDLE;
105     };
106 
107     /**
108      * Call from derived class when instantiation / initialization is successful
109      */
baseRegisterPlayer()110     protected void baseRegisterPlayer() {
111         if (!USE_AUDIOFLINGER_MUTING_FOR_OP) {
112             IBinder b = ServiceManager.getService(Context.APP_OPS_SERVICE);
113             mAppOps = IAppOpsService.Stub.asInterface(b);
114             // initialize mHasAppOpsPlayAudio
115             updateAppOpsPlayAudio();
116             // register a callback to monitor whether the OP_PLAY_AUDIO is still allowed
117             mAppOpsCallback = new IAppOpsCallbackWrapper(this);
118             try {
119                 mAppOps.startWatchingMode(AppOpsManager.OP_PLAY_AUDIO,
120                         ActivityThread.currentPackageName(), mAppOpsCallback);
121             } catch (RemoteException e) {
122                 Log.e(TAG, "Error registering appOps callback", e);
123                 mHasAppOpsPlayAudio = false;
124             }
125         }
126         try {
127             mPlayerIId = getService().trackPlayer(
128                     new PlayerIdCard(mImplType, mAttributes, new IPlayerWrapper(this)));
129         } catch (RemoteException e) {
130             Log.e(TAG, "Error talking to audio service, player will not be tracked", e);
131         }
132     }
133 
134     /**
135      * To be called whenever the audio attributes of the player change
136      * @param attr non-null audio attributes
137      */
baseUpdateAudioAttributes(@onNull AudioAttributes attr)138     void baseUpdateAudioAttributes(@NonNull AudioAttributes attr) {
139         if (attr == null) {
140             throw new IllegalArgumentException("Illegal null AudioAttributes");
141         }
142         try {
143             getService().playerAttributes(mPlayerIId, attr);
144         } catch (RemoteException e) {
145             Log.e(TAG, "Error talking to audio service, STARTED state will not be tracked", e);
146         }
147         synchronized (mLock) {
148             boolean attributesChanged = (mAttributes != attr);
149             mAttributes = attr;
150             updateAppOpsPlayAudio_sync(attributesChanged);
151         }
152     }
153 
updateState(int state)154     private void updateState(int state) {
155         final int piid;
156         synchronized (mLock) {
157             mState = state;
158             piid = mPlayerIId;
159         }
160         try {
161             getService().playerEvent(piid, state);
162         } catch (RemoteException e) {
163             Log.e(TAG, "Error talking to audio service, "
164                     + AudioPlaybackConfiguration.toLogFriendlyPlayerState(state)
165                     + " state will not be tracked for piid=" + piid, e);
166         }
167     }
168 
baseStart()169     void baseStart() {
170         if (DEBUG) { Log.v(TAG, "baseStart() piid=" + mPlayerIId); }
171         updateState(AudioPlaybackConfiguration.PLAYER_STATE_STARTED);
172         synchronized (mLock) {
173             if (isRestricted_sync()) {
174                 playerSetVolume(true/*muting*/,0, 0);
175             }
176         }
177     }
178 
baseSetStartDelayMs(int delayMs)179     void baseSetStartDelayMs(int delayMs) {
180         synchronized(mLock) {
181             mStartDelayMs = Math.max(delayMs, 0);
182         }
183     }
184 
getStartDelayMs()185     protected int getStartDelayMs() {
186         synchronized(mLock) {
187             return mStartDelayMs;
188         }
189     }
190 
basePause()191     void basePause() {
192         if (DEBUG) { Log.v(TAG, "basePause() piid=" + mPlayerIId); }
193         updateState(AudioPlaybackConfiguration.PLAYER_STATE_PAUSED);
194     }
195 
baseStop()196     void baseStop() {
197         if (DEBUG) { Log.v(TAG, "baseStop() piid=" + mPlayerIId); }
198         updateState(AudioPlaybackConfiguration.PLAYER_STATE_STOPPED);
199     }
200 
baseSetPan(float pan)201     void baseSetPan(float pan) {
202         final float p = Math.min(Math.max(-1.0f, pan), 1.0f);
203         synchronized (mLock) {
204             if (p >= 0.0f) {
205                 mPanMultiplierL = 1.0f - p;
206                 mPanMultiplierR = 1.0f;
207             } else {
208                 mPanMultiplierL = 1.0f;
209                 mPanMultiplierR = 1.0f + p;
210             }
211         }
212         updatePlayerVolume();
213     }
214 
updatePlayerVolume()215     private void updatePlayerVolume() {
216         final float finalLeftVol, finalRightVol;
217         final boolean isRestricted;
218         synchronized (mLock) {
219             finalLeftVol = mVolMultiplier * mLeftVolume * mPanMultiplierL;
220             finalRightVol = mVolMultiplier * mRightVolume * mPanMultiplierR;
221             isRestricted = isRestricted_sync();
222         }
223         playerSetVolume(isRestricted /*muting*/, finalLeftVol, finalRightVol);
224     }
225 
setVolumeMultiplier(float vol)226     void setVolumeMultiplier(float vol) {
227         synchronized (mLock) {
228             this.mVolMultiplier = vol;
229         }
230         updatePlayerVolume();
231     }
232 
baseSetVolume(float leftVolume, float rightVolume)233     void baseSetVolume(float leftVolume, float rightVolume) {
234         synchronized (mLock) {
235             mLeftVolume = leftVolume;
236             mRightVolume = rightVolume;
237         }
238         updatePlayerVolume();
239     }
240 
baseSetAuxEffectSendLevel(float level)241     int baseSetAuxEffectSendLevel(float level) {
242         synchronized (mLock) {
243             mAuxEffectSendLevel = level;
244             if (isRestricted_sync()) {
245                 return AudioSystem.SUCCESS;
246             }
247         }
248         return playerSetAuxEffectSendLevel(false/*muting*/, level);
249     }
250 
251     /**
252      * To be called from a subclass release or finalize method.
253      * Releases AppOps related resources.
254      */
baseRelease()255     void baseRelease() {
256         if (DEBUG) { Log.v(TAG, "baseRelease() piid=" + mPlayerIId + " state=" + mState); }
257         boolean releasePlayer = false;
258         synchronized (mLock) {
259             if (mState != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) {
260                 releasePlayer = true;
261                 mState = AudioPlaybackConfiguration.PLAYER_STATE_RELEASED;
262             }
263         }
264         try {
265             if (releasePlayer) {
266                 getService().releasePlayer(mPlayerIId);
267             }
268         } catch (RemoteException e) {
269             Log.e(TAG, "Error talking to audio service, the player will still be tracked", e);
270         }
271         try {
272             if (mAppOps != null) {
273                 mAppOps.stopWatchingMode(mAppOpsCallback);
274             }
275         } catch (Exception e) {
276             // nothing to do here, the object is supposed to be released anyway
277         }
278     }
279 
updateAppOpsPlayAudio()280     private void updateAppOpsPlayAudio() {
281         synchronized (mLock) {
282             updateAppOpsPlayAudio_sync(false);
283         }
284     }
285 
286     /**
287      * To be called whenever a condition that might affect audibility of this player is updated.
288      * Must be called synchronized on mLock.
289      */
updateAppOpsPlayAudio_sync(boolean attributesChanged)290     void updateAppOpsPlayAudio_sync(boolean attributesChanged) {
291         if (USE_AUDIOFLINGER_MUTING_FOR_OP) {
292             return;
293         }
294         boolean oldHasAppOpsPlayAudio = mHasAppOpsPlayAudio;
295         try {
296             int mode = AppOpsManager.MODE_IGNORED;
297             if (mAppOps != null) {
298                 mode = mAppOps.checkAudioOperation(AppOpsManager.OP_PLAY_AUDIO,
299                     mAttributes.getUsage(),
300                     Process.myUid(), ActivityThread.currentPackageName());
301             }
302             mHasAppOpsPlayAudio = (mode == AppOpsManager.MODE_ALLOWED);
303         } catch (RemoteException e) {
304             mHasAppOpsPlayAudio = false;
305         }
306 
307         // AppsOps alters a player's volume; when the restriction changes, reflect it on the actual
308         // volume used by the player
309         try {
310             if (oldHasAppOpsPlayAudio != mHasAppOpsPlayAudio ||
311                     attributesChanged) {
312                 getService().playerHasOpPlayAudio(mPlayerIId, mHasAppOpsPlayAudio);
313                 if (!isRestricted_sync()) {
314                     if (DEBUG_APP_OPS) {
315                         Log.v(TAG, "updateAppOpsPlayAudio: unmuting player, vol=" + mLeftVolume
316                                 + "/" + mRightVolume);
317                     }
318                     playerSetVolume(false/*muting*/,
319                             mLeftVolume * mPanMultiplierL, mRightVolume * mPanMultiplierR);
320                     playerSetAuxEffectSendLevel(false/*muting*/, mAuxEffectSendLevel);
321                 } else {
322                     if (DEBUG_APP_OPS) {
323                         Log.v(TAG, "updateAppOpsPlayAudio: muting player");
324                     }
325                     playerSetVolume(true/*muting*/, 0.0f, 0.0f);
326                     playerSetAuxEffectSendLevel(true/*muting*/, 0.0f);
327                 }
328             }
329         } catch (Exception e) {
330             // failing silently, player might not be in right state
331         }
332     }
333 
334     /**
335      * To be called by the subclass whenever an operation is potentially restricted.
336      * As the media player-common behavior are incorporated into this class, the subclass's need
337      * to call this method should be removed, and this method could become private.
338      * FIXME can this method be private so subclasses don't have to worry about when to check
339      *    the restrictions.
340      * @return
341      */
isRestricted_sync()342     boolean isRestricted_sync() {
343         if (USE_AUDIOFLINGER_MUTING_FOR_OP) {
344             return false;
345         }
346         // check app ops
347         if (mHasAppOpsPlayAudio) {
348             return false;
349         }
350         // check bypass flag
351         if ((mAttributes.getAllFlags() & AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY) != 0) {
352             return false;
353         }
354         // check force audibility flag and camera restriction
355         if (((mAttributes.getAllFlags() & AudioAttributes.FLAG_AUDIBILITY_ENFORCED) != 0)
356                 && (mAttributes.getUsage() == AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)) {
357             boolean cameraSoundForced = false;
358             try {
359                 cameraSoundForced = getService().isCameraSoundForced();
360             } catch (RemoteException e) {
361                 Log.e(TAG, "Cannot access AudioService in isRestricted_sync()");
362             } catch (NullPointerException e) {
363                 Log.e(TAG, "Null AudioService in isRestricted_sync()");
364             }
365             if (cameraSoundForced) {
366                 return false;
367             }
368         }
369         return true;
370     }
371 
getService()372     private static IAudioService getService()
373     {
374         if (sService != null) {
375             return sService;
376         }
377         IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
378         sService = IAudioService.Stub.asInterface(b);
379         return sService;
380     }
381 
382     /**
383      * @hide
384      * @param delayMs
385      */
setStartDelayMs(int delayMs)386     public void setStartDelayMs(int delayMs) {
387         baseSetStartDelayMs(delayMs);
388     }
389 
390     //=====================================================================
391     // Abstract methods a subclass needs to implement
392     /**
393      * Abstract method for the subclass behavior's for volume and muting commands
394      * @param muting if true, the player is to be muted, and the volume values can be ignored
395      * @param leftVolume the left volume to use if muting is false
396      * @param rightVolume the right volume to use if muting is false
397      */
playerSetVolume(boolean muting, float leftVolume, float rightVolume)398     abstract void playerSetVolume(boolean muting, float leftVolume, float rightVolume);
399 
400     /**
401      * Abstract method to apply a {@link VolumeShaper.Configuration}
402      * and a {@link VolumeShaper.Operation} to the Player.
403      * This should be overridden by the Player to call into the native
404      * VolumeShaper implementation. Multiple {@code VolumeShapers} may be
405      * concurrently active for a given Player, each accessible by the
406      * {@code VolumeShaper} id.
407      *
408      * The {@code VolumeShaper} implementation caches the id returned
409      * when applying a fully specified configuration
410      * from {VolumeShaper.Configuration.Builder} to track later
411      * operation changes requested on it.
412      *
413      * @param configuration a {@code VolumeShaper.Configuration} object
414      *        created by {@link VolumeShaper.Configuration.Builder} or
415      *        an created from a {@code VolumeShaper} id
416      *        by the {@link VolumeShaper.Configuration} constructor.
417      * @param operation a {@code VolumeShaper.Operation}.
418      * @return a negative error status or a
419      *         non-negative {@code VolumeShaper} id on success.
420      */
playerApplyVolumeShaper( @onNull VolumeShaper.Configuration configuration, @NonNull VolumeShaper.Operation operation)421     /* package */ abstract int playerApplyVolumeShaper(
422             @NonNull VolumeShaper.Configuration configuration,
423             @NonNull VolumeShaper.Operation operation);
424 
425     /**
426      * Abstract method to get the current VolumeShaper state.
427      * @param id the {@code VolumeShaper} id returned from
428      *           sending a fully specified {@code VolumeShaper.Configuration}
429      *           through {@link #playerApplyVolumeShaper}
430      * @return a {@code VolumeShaper.State} object or null if
431      *         there is no {@code VolumeShaper} for the id.
432      */
playerGetVolumeShaperState(int id)433     /* package */ abstract @Nullable VolumeShaper.State playerGetVolumeShaperState(int id);
434 
playerSetAuxEffectSendLevel(boolean muting, float level)435     abstract int playerSetAuxEffectSendLevel(boolean muting, float level);
playerStart()436     abstract void playerStart();
playerPause()437     abstract void playerPause();
playerStop()438     abstract void playerStop();
439 
440     //=====================================================================
441     private static class IAppOpsCallbackWrapper extends IAppOpsCallback.Stub {
442         private final WeakReference<PlayerBase> mWeakPB;
443 
IAppOpsCallbackWrapper(PlayerBase pb)444         public IAppOpsCallbackWrapper(PlayerBase pb) {
445             mWeakPB = new WeakReference<PlayerBase>(pb);
446         }
447 
448         @Override
opChanged(int op, int uid, String packageName)449         public void opChanged(int op, int uid, String packageName) {
450             if (op == AppOpsManager.OP_PLAY_AUDIO) {
451                 if (DEBUG_APP_OPS) { Log.v(TAG, "opChanged: op=PLAY_AUDIO pack=" + packageName); }
452                 final PlayerBase pb = mWeakPB.get();
453                 if (pb != null) {
454                     pb.updateAppOpsPlayAudio();
455                 }
456             }
457         }
458     }
459 
460     //=====================================================================
461     /**
462      * Wrapper around an implementation of IPlayer for all subclasses of PlayerBase
463      * that doesn't keep a strong reference on PlayerBase
464      */
465     private static class IPlayerWrapper extends IPlayer.Stub {
466         private final WeakReference<PlayerBase> mWeakPB;
467 
IPlayerWrapper(PlayerBase pb)468         public IPlayerWrapper(PlayerBase pb) {
469             mWeakPB = new WeakReference<PlayerBase>(pb);
470         }
471 
472         @Override
start()473         public void start() {
474             final PlayerBase pb = mWeakPB.get();
475             if (pb != null) {
476                 pb.playerStart();
477             }
478         }
479 
480         @Override
pause()481         public void pause() {
482             final PlayerBase pb = mWeakPB.get();
483             if (pb != null) {
484                 pb.playerPause();
485             }
486         }
487 
488         @Override
stop()489         public void stop() {
490             final PlayerBase pb = mWeakPB.get();
491             if (pb != null) {
492                 pb.playerStop();
493             }
494         }
495 
496         @Override
setVolume(float vol)497         public void setVolume(float vol) {
498             final PlayerBase pb = mWeakPB.get();
499             if (pb != null) {
500                 pb.setVolumeMultiplier(vol);
501             }
502         }
503 
504         @Override
setPan(float pan)505         public void setPan(float pan) {
506             final PlayerBase pb = mWeakPB.get();
507             if (pb != null) {
508                 pb.baseSetPan(pan);
509             }
510         }
511 
512         @Override
setStartDelayMs(int delayMs)513         public void setStartDelayMs(int delayMs) {
514             final PlayerBase pb = mWeakPB.get();
515             if (pb != null) {
516                 pb.baseSetStartDelayMs(delayMs);
517             }
518         }
519 
520         @Override
applyVolumeShaper( @onNull VolumeShaper.Configuration configuration, @NonNull VolumeShaper.Operation operation)521         public void applyVolumeShaper(
522                 @NonNull VolumeShaper.Configuration configuration,
523                 @NonNull VolumeShaper.Operation operation) {
524             final PlayerBase pb = mWeakPB.get();
525             if (pb != null) {
526                 pb.playerApplyVolumeShaper(configuration, operation);
527             }
528         }
529     }
530 
531     //=====================================================================
532     /**
533      * Class holding all the information about a player that needs to be known at registration time
534      */
535     public static class PlayerIdCard implements Parcelable {
536         public final int mPlayerType;
537 
538         public static final int AUDIO_ATTRIBUTES_NONE = 0;
539         public static final int AUDIO_ATTRIBUTES_DEFINED = 1;
540         public final AudioAttributes mAttributes;
541         public final IPlayer mIPlayer;
542 
PlayerIdCard(int type, @NonNull AudioAttributes attr, @NonNull IPlayer iplayer)543         PlayerIdCard(int type, @NonNull AudioAttributes attr, @NonNull IPlayer iplayer) {
544             mPlayerType = type;
545             mAttributes = attr;
546             mIPlayer = iplayer;
547         }
548 
549         @Override
hashCode()550         public int hashCode() {
551             return Objects.hash(mPlayerType);
552         }
553 
554         @Override
describeContents()555         public int describeContents() {
556             return 0;
557         }
558 
559         @Override
writeToParcel(Parcel dest, int flags)560         public void writeToParcel(Parcel dest, int flags) {
561             dest.writeInt(mPlayerType);
562             mAttributes.writeToParcel(dest, 0);
563             dest.writeStrongBinder(mIPlayer == null ? null : mIPlayer.asBinder());
564         }
565 
566         public static final @android.annotation.NonNull Parcelable.Creator<PlayerIdCard> CREATOR
567         = new Parcelable.Creator<PlayerIdCard>() {
568             /**
569              * Rebuilds an PlayerIdCard previously stored with writeToParcel().
570              * @param p Parcel object to read the PlayerIdCard from
571              * @return a new PlayerIdCard created from the data in the parcel
572              */
573             public PlayerIdCard createFromParcel(Parcel p) {
574                 return new PlayerIdCard(p);
575             }
576             public PlayerIdCard[] newArray(int size) {
577                 return new PlayerIdCard[size];
578             }
579         };
580 
PlayerIdCard(Parcel in)581         private PlayerIdCard(Parcel in) {
582             mPlayerType = in.readInt();
583             mAttributes = AudioAttributes.CREATOR.createFromParcel(in);
584             // IPlayer can be null if unmarshalling a Parcel coming from who knows where
585             final IBinder b = in.readStrongBinder();
586             mIPlayer = (b == null ? null : IPlayer.Stub.asInterface(b));
587         }
588 
589         @Override
equals(Object o)590         public boolean equals(Object o) {
591             if (this == o) return true;
592             if (o == null || !(o instanceof PlayerIdCard)) return false;
593 
594             PlayerIdCard that = (PlayerIdCard) o;
595 
596             // FIXME change to the binder player interface once supported as a member
597             return ((mPlayerType == that.mPlayerType) && mAttributes.equals(that.mAttributes));
598         }
599     }
600 
601     //=====================================================================
602     // Utilities
603 
604     /**
605      * @hide
606      * Use to generate warning or exception in legacy code paths that allowed passing stream types
607      * to qualify audio playback.
608      * @param streamType the stream type to check
609      * @throws IllegalArgumentException
610      */
deprecateStreamTypeForPlayback(int streamType, @NonNull String className, @NonNull String opName)611     public static void deprecateStreamTypeForPlayback(int streamType, @NonNull String className,
612             @NonNull String opName) throws IllegalArgumentException {
613         // STREAM_ACCESSIBILITY was introduced at the same time the use of stream types
614         // for audio playback was deprecated, so it is not allowed at all to qualify a playback
615         // use case
616         if (streamType == AudioManager.STREAM_ACCESSIBILITY) {
617             throw new IllegalArgumentException("Use of STREAM_ACCESSIBILITY is reserved for "
618                     + "volume control");
619         }
620         Log.w(className, "Use of stream types is deprecated for operations other than " +
621                 "volume control");
622         Log.w(className, "See the documentation of " + opName + " for what to use instead with " +
623                 "android.media.AudioAttributes to qualify your playback use case");
624     }
625 }
626