Add workaround to capture audio on Android 11
On Android 11, it is possible to start the capture only when the running app is in foreground. But scrcpy is not an app, it's a Java application started from shell. As a workaround, start an existing Android shell existing activity just to start the capture, then close it immediately. PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757> Co-authored-by: Romain Vimont <rom@rom1v.com> Signed-off-by: Romain Vimont <rom@rom1v.com>
This commit is contained in:
parent
b60a8aa657
commit
de40cac6ad
3 changed files with 114 additions and 4 deletions
|
@ -0,0 +1,7 @@
|
||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown if audio capture failed on Android 11 specifically because the running App (shell) was not in foreground.
|
||||||
|
*/
|
||||||
|
public class AudioCaptureForegroundException extends Exception {
|
||||||
|
}
|
|
@ -1,7 +1,11 @@
|
||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Intent;
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioRecord;
|
import android.media.AudioRecord;
|
||||||
import android.media.AudioTimestamp;
|
import android.media.AudioTimestamp;
|
||||||
|
@ -12,6 +16,7 @@ import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
@ -179,7 +184,7 @@ public final class AudioEncoder {
|
||||||
thread = new Thread(() -> {
|
thread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
encode();
|
encode();
|
||||||
} catch (ConfigurationException e) {
|
} catch (ConfigurationException | AudioCaptureForegroundException e) {
|
||||||
// Do not print stack trace, a user-friendly error-message has already been logged
|
// Do not print stack trace, a user-friendly error-message has already been logged
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Ln.e("Audio encoding error", e);
|
Ln.e("Audio encoding error", e);
|
||||||
|
@ -218,8 +223,34 @@ public final class AudioEncoder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void startWorkaroundAndroid11() {
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
// Android 11 requires Apps to be at foreground to record audio.
|
||||||
|
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
|
||||||
|
// But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android
|
||||||
|
// shell ("com.android.shell").
|
||||||
|
// If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the
|
||||||
|
// foreground.
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||||
|
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
|
||||||
|
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
|
||||||
|
// Wait for activity to start
|
||||||
|
SystemClock.sleep(150);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void stopWorkaroundAndroid11() {
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.M)
|
@TargetApi(Build.VERSION_CODES.M)
|
||||||
public void encode() throws IOException, ConfigurationException {
|
public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
Ln.w("Audio disabled: it is not supported before Android 11");
|
Ln.w("Audio disabled: it is not supported before Android 11");
|
||||||
streamer.writeDisableStream(false);
|
streamer.writeDisableStream(false);
|
||||||
|
@ -242,8 +273,20 @@ public final class AudioEncoder {
|
||||||
mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper()));
|
mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper()));
|
||||||
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||||
|
|
||||||
recorder = createAudioRecord();
|
startWorkaroundAndroid11();
|
||||||
recorder.startRecording();
|
try {
|
||||||
|
recorder = createAudioRecord();
|
||||||
|
recorder.startRecording();
|
||||||
|
} catch (UnsupportedOperationException e) {
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
Ln.e("Failed to start audio capture");
|
||||||
|
Ln.e("On Android 11, it is only possible to capture in foreground, make sure that the device is unlocked when starting scrcpy.");
|
||||||
|
throw new AudioCaptureForegroundException();
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
stopWorkaroundAndroid11();
|
||||||
|
}
|
||||||
recorderStarted = true;
|
recorderStarted = true;
|
||||||
|
|
||||||
final MediaCodec mediaCodecRef = mediaCodec;
|
final MediaCodec mediaCodecRef = mediaCodec;
|
||||||
|
|
|
@ -3,7 +3,12 @@ package com.genymobile.scrcpy.wrappers;
|
||||||
import com.genymobile.scrcpy.FakeContext;
|
import com.genymobile.scrcpy.FakeContext;
|
||||||
import com.genymobile.scrcpy.Ln;
|
import com.genymobile.scrcpy.Ln;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Intent;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
|
|
||||||
|
@ -11,12 +16,15 @@ import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||||
public class ActivityManager {
|
public class ActivityManager {
|
||||||
|
|
||||||
private final IInterface manager;
|
private final IInterface manager;
|
||||||
private Method getContentProviderExternalMethod;
|
private Method getContentProviderExternalMethod;
|
||||||
private boolean getContentProviderExternalMethodNewVersion = true;
|
private boolean getContentProviderExternalMethodNewVersion = true;
|
||||||
private Method removeContentProviderExternalMethod;
|
private Method removeContentProviderExternalMethod;
|
||||||
|
private Method startActivityAsUserWithFeatureMethod;
|
||||||
|
private Method forceStopPackageMethod;
|
||||||
|
|
||||||
public ActivityManager(IInterface manager) {
|
public ActivityManager(IInterface manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
|
@ -43,6 +51,7 @@ public class ActivityManager {
|
||||||
return removeContentProviderExternalMethod;
|
return removeContentProviderExternalMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.Q)
|
||||||
private ContentProvider getContentProviderExternal(String name, IBinder token) {
|
private ContentProvider getContentProviderExternal(String name, IBinder token) {
|
||||||
try {
|
try {
|
||||||
Method method = getGetContentProviderExternalMethod();
|
Method method = getGetContentProviderExternalMethod();
|
||||||
|
@ -85,4 +94,55 @@ public class ActivityManager {
|
||||||
public ContentProvider createSettingsProvider() {
|
public ContentProvider createSettingsProvider() {
|
||||||
return getContentProviderExternal("settings", new Binder());
|
return getContentProviderExternal("settings", new Binder());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Method getStartActivityAsUserWithFeatureMethod() throws NoSuchMethodException, ClassNotFoundException {
|
||||||
|
if (startActivityAsUserWithFeatureMethod == null) {
|
||||||
|
Class<?> iApplicationThreadClass = Class.forName("android.app.IApplicationThread");
|
||||||
|
Class<?> profilerInfo = Class.forName("android.app.ProfilerInfo");
|
||||||
|
startActivityAsUserWithFeatureMethod = manager.getClass()
|
||||||
|
.getMethod("startActivityAsUserWithFeature", iApplicationThreadClass, String.class, String.class, Intent.class, String.class,
|
||||||
|
IBinder.class, String.class, int.class, int.class, profilerInfo, Bundle.class, int.class);
|
||||||
|
}
|
||||||
|
return startActivityAsUserWithFeatureMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("ConstantConditions")
|
||||||
|
public int startActivityAsUserWithFeature(Intent intent) {
|
||||||
|
try {
|
||||||
|
Method method = getStartActivityAsUserWithFeatureMethod();
|
||||||
|
return (int) method.invoke(
|
||||||
|
/* this */ manager,
|
||||||
|
/* caller */ null,
|
||||||
|
/* callingPackage */ FakeContext.PACKAGE_NAME,
|
||||||
|
/* callingFeatureId */ null,
|
||||||
|
/* intent */ intent,
|
||||||
|
/* resolvedType */ null,
|
||||||
|
/* resultTo */ null,
|
||||||
|
/* resultWho */ null,
|
||||||
|
/* requestCode */ 0,
|
||||||
|
/* startFlags */ 0,
|
||||||
|
/* profilerInfo */ null,
|
||||||
|
/* bOptions */ null,
|
||||||
|
/* userId */ /* UserHandle.USER_CURRENT */ -2);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Ln.e("Could not invoke method", e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method getForceStopPackageMethod() throws NoSuchMethodException {
|
||||||
|
if (forceStopPackageMethod == null) {
|
||||||
|
forceStopPackageMethod = manager.getClass().getMethod("forceStopPackage", String.class, int.class);
|
||||||
|
}
|
||||||
|
return forceStopPackageMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forceStopPackage(String packageName) {
|
||||||
|
try {
|
||||||
|
Method method = getForceStopPackageMethod();
|
||||||
|
method.invoke(manager, packageName, /* userId */ /* UserHandle.USER_CURRENT */ -2);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Ln.e("Could not invoke method", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue