Load the Spatializer API with reflection

This change adds a delegate class that loads and forwards calls
to a Spatializer with reflection, so that we can use the Spatializer
API before we update the compile SDK target to 32.

PiperOrigin-RevId: 416027289
This commit is contained in:
christosts 2021-12-13 14:25:18 +00:00 committed by tonihei
parent a4f67ccf6a
commit d90f5f37ef

View File

@ -0,0 +1,217 @@
/*
* Copyright 2021 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 androidx.media3.exoplayer.audio;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
/**
* Exposes the android.media.Spatializer API via reflection. This is so that we can use the
* Spatializer while the compile SDK target is set to 31.
*/
@RequiresApi(31)
/* package */ final class SpatializerDelegate {
/** Level of support for audio spatialization. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@IntDef({
SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL,
SPATIALIZER_IMMERSIVE_LEVEL_NONE,
SPATIALIZER_IMMERSIVE_LEVEL_OTHER
})
@interface ImmersiveAudioLevel {}
/** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL */
public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1;
/** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_NONE */
public static final int SPATIALIZER_IMMERSIVE_LEVEL_NONE = 0;
/** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_OTHER */
public static final int SPATIALIZER_IMMERSIVE_LEVEL_OTHER = -1;
/** Wrapper for Spatializer.OnSpatializerStateChangedListener */
public interface Listener {
/** See Spatializer.OnSpatializerStateChangedListener.onSpatializerEnabledChanged */
void onSpatializerEnabledChanged(SpatializerDelegate spatializer, boolean enabled);
/** See Spatializer.OnSpatializerStateChangedListener.onSpatializerAvailableChanged */
void onSpatializerAvailableChanged(SpatializerDelegate spatializer, boolean available);
}
private final Object spatializer;
private final Class<?> spatializerClass;
private final Class<?> spatializerListenerClass;
private final Method isEnabled;
private final Method isAvailable;
private final Method getImmersiveAudioLevel;
private final Method canBeSpatialized;
private final Method addListener;
private final Method removeListener;
private final Map<Listener, Object> listeners;
/** Creates an instance. */
public SpatializerDelegate(Context context)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
IllegalAccessException {
Method getSpatializerMethod = AudioManager.class.getMethod("getSpatializer");
AudioManager manager =
Assertions.checkNotNull(
(AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE));
spatializer = checkStateNotNull(getSpatializerMethod.invoke(manager));
spatializerClass = Class.forName("android.media.Spatializer");
spatializerListenerClass =
Class.forName("android.media.Spatializer$OnSpatializerStateChangedListener");
isEnabled = spatializerClass.getMethod("isEnabled");
isAvailable = spatializerClass.getMethod("isAvailable");
getImmersiveAudioLevel = spatializerClass.getMethod("getImmersiveAudioLevel");
canBeSpatialized =
spatializerClass.getMethod(
"canBeSpatialized", android.media.AudioAttributes.class, AudioFormat.class);
addListener =
spatializerClass.getMethod(
"addOnSpatializerStateChangedListener", Executor.class, spatializerListenerClass);
removeListener =
spatializerClass.getMethod(
"removeOnSpatializerStateChangedListener", spatializerListenerClass);
listeners = new HashMap<>();
}
/** Delegates to Spatializer.isEnabled() */
public boolean isEnabled() {
try {
return (boolean) Util.castNonNull(isEnabled.invoke(spatializer));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.isAvailable() */
public boolean isAvailable() {
try {
return (boolean) Util.castNonNull(isAvailable.invoke(spatializer));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.getImmersiveAudioLevel() */
@ImmersiveAudioLevel
public int getImmersiveAudioLevel() {
try {
return (int) Util.castNonNull(getImmersiveAudioLevel.invoke(spatializer));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.canBeSpatialized() */
public boolean canBeSpatialized(AudioAttributes attributes, AudioFormat format) {
try {
return (boolean) Util.castNonNull(canBeSpatialized.invoke(spatializer, attributes, format));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.addOnSpatializerStateChangedListener() */
public void addOnSpatializerStateChangedListener(Executor executor, Listener listener) {
if (listeners.containsKey(listener)) {
return;
}
Object listenerProxy = createSpatializerListenerProxy(listener);
try {
addListener.invoke(spatializer, executor, listenerProxy);
listeners.put(listener, listenerProxy);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.removeOnSpatializerStateChangedListener() */
public void removeOnSpatializerStateChangedListener(Listener listener) {
@Nullable Object proxy = listeners.get(listener);
if (proxy == null) {
return;
}
try {
removeListener.invoke(spatializer, proxy);
listeners.remove(listener);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
private Object createSpatializerListenerProxy(Listener listener) {
return Proxy.newProxyInstance(
spatializerListenerClass.getClassLoader(),
new Class<?>[] {spatializerListenerClass},
new ProxySpatializerListener(this, listener));
}
/** Proxy-based implementation of Spatializer.OnSpatializerStateChangedListener. */
private static final class ProxySpatializerListener implements InvocationHandler {
private final SpatializerDelegate spatializerDelegate;
private final Listener listener;
private ProxySpatializerListener(SpatializerDelegate spatializerDelegate, Listener listener) {
this.spatializerDelegate = spatializerDelegate;
this.listener = listener;
}
@Override
public Object invoke(Object o, Method method, Object[] objects) {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
if (methodName.equals("onSpatializerAvailableChanged")
&& parameterTypes.length == 2
&& spatializerDelegate.spatializerClass.isAssignableFrom(parameterTypes[0])
&& parameterTypes[1].equals(Boolean.TYPE)) {
listener.onSpatializerAvailableChanged(spatializerDelegate, (boolean) objects[1]);
} else if (methodName.equals("onSpatializerEnabledChanged")
&& parameterTypes.length == 2
&& spatializerDelegate.spatializerClass.isAssignableFrom(parameterTypes[0])
&& parameterTypes[1].equals(Boolean.TYPE)) {
listener.onSpatializerEnabledChanged(spatializerDelegate, (boolean) objects[1]);
}
return this;
}
}
}