Skip to content

Commit 2a6df62

Browse files
yume-chanrom1v
andcommitted
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 <Genymobile/scrcpy#3757> Co-authored-by: Romain Vimont <[email protected]> Signed-off-by: Romain Vimont <[email protected]>
1 parent 30d1244 commit 2a6df62

File tree

3 files changed

+114
-4
lines changed

3 files changed

+114
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.genymobile.scrcpy;
2+
3+
/**
4+
* Exception thrown if audio capture failed on Android 11 specifically because the running App (shell) was not in foreground.
5+
*/
6+
public class AudioCaptureForegroundException extends Exception {
7+
}

server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java

+47-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package com.genymobile.scrcpy;
22

3+
import com.genymobile.scrcpy.wrappers.ServiceManager;
4+
35
import android.annotation.SuppressLint;
46
import android.annotation.TargetApi;
7+
import android.content.ComponentName;
8+
import android.content.Intent;
59
import android.media.AudioFormat;
610
import android.media.AudioRecord;
711
import android.media.AudioTimestamp;
@@ -12,6 +16,7 @@
1216
import android.os.Handler;
1317
import android.os.HandlerThread;
1418
import android.os.Looper;
19+
import android.os.SystemClock;
1520

1621
import java.io.IOException;
1722
import java.nio.ByteBuffer;
@@ -179,7 +184,7 @@ public void start() {
179184
thread = new Thread(() -> {
180185
try {
181186
encode();
182-
} catch (ConfigurationException e) {
187+
} catch (ConfigurationException | AudioCaptureForegroundException e) {
183188
// Do not print stack trace, a user-friendly error-message has already been logged
184189
} catch (IOException e) {
185190
Ln.e("Audio encoding error", e);
@@ -218,8 +223,34 @@ private synchronized void waitEnded() {
218223
}
219224
}
220225

226+
private static void startWorkaroundAndroid11() {
227+
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
228+
// Android 11 requires Apps to be at foreground to record audio.
229+
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
230+
// 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
231+
// shell ("com.android.shell").
232+
// If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the
233+
// foreground.
234+
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
235+
Intent intent = new Intent(Intent.ACTION_MAIN);
236+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
237+
intent.addCategory(Intent.CATEGORY_LAUNCHER);
238+
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
239+
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
240+
// Wait for activity to start
241+
SystemClock.sleep(150);
242+
}
243+
}
244+
}
245+
246+
private static void stopWorkaroundAndroid11() {
247+
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
248+
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
249+
}
250+
}
251+
221252
@TargetApi(Build.VERSION_CODES.M)
222-
public void encode() throws IOException, ConfigurationException {
253+
public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException {
223254
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
224255
Ln.w("Audio disabled: it is not supported before Android 11");
225256
streamer.writeDisableStream(false);
@@ -242,8 +273,20 @@ public void encode() throws IOException, ConfigurationException {
242273
mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper()));
243274
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
244275

245-
recorder = createAudioRecord();
246-
recorder.startRecording();
276+
startWorkaroundAndroid11();
277+
try {
278+
recorder = createAudioRecord();
279+
recorder.startRecording();
280+
} catch (UnsupportedOperationException e) {
281+
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
282+
Ln.e("Failed to start audio capture");
283+
Ln.e("On Android 11, it is only possible to capture in foreground, make sure that the device is unlocked when starting scrcpy.");
284+
throw new AudioCaptureForegroundException();
285+
}
286+
throw e;
287+
} finally {
288+
stopWorkaroundAndroid11();
289+
}
247290
recorderStarted = true;
248291

249292
final MediaCodec mediaCodecRef = mediaCodec;

server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java

+60
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,28 @@
33
import com.genymobile.scrcpy.FakeContext;
44
import com.genymobile.scrcpy.Ln;
55

6+
import android.annotation.SuppressLint;
7+
import android.annotation.TargetApi;
8+
import android.content.Intent;
69
import android.os.Binder;
10+
import android.os.Build;
11+
import android.os.Bundle;
712
import android.os.IBinder;
813
import android.os.IInterface;
914

1015
import java.lang.reflect.Field;
1116
import java.lang.reflect.InvocationTargetException;
1217
import java.lang.reflect.Method;
1318

19+
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
1420
public class ActivityManager {
1521

1622
private final IInterface manager;
1723
private Method getContentProviderExternalMethod;
1824
private boolean getContentProviderExternalMethodNewVersion = true;
1925
private Method removeContentProviderExternalMethod;
26+
private Method startActivityAsUserWithFeatureMethod;
27+
private Method forceStopPackageMethod;
2028

2129
public ActivityManager(IInterface manager) {
2230
this.manager = manager;
@@ -43,6 +51,7 @@ private Method getRemoveContentProviderExternalMethod() throws NoSuchMethodExcep
4351
return removeContentProviderExternalMethod;
4452
}
4553

54+
@TargetApi(Build.VERSION_CODES.Q)
4655
private ContentProvider getContentProviderExternal(String name, IBinder token) {
4756
try {
4857
Method method = getGetContentProviderExternalMethod();
@@ -85,4 +94,55 @@ void removeContentProviderExternal(String name, IBinder token) {
8594
public ContentProvider createSettingsProvider() {
8695
return getContentProviderExternal("settings", new Binder());
8796
}
97+
98+
private Method getStartActivityAsUserWithFeatureMethod() throws NoSuchMethodException, ClassNotFoundException {
99+
if (startActivityAsUserWithFeatureMethod == null) {
100+
Class<?> iApplicationThreadClass = Class.forName("android.app.IApplicationThread");
101+
Class<?> profilerInfo = Class.forName("android.app.ProfilerInfo");
102+
startActivityAsUserWithFeatureMethod = manager.getClass()
103+
.getMethod("startActivityAsUserWithFeature", iApplicationThreadClass, String.class, String.class, Intent.class, String.class,
104+
IBinder.class, String.class, int.class, int.class, profilerInfo, Bundle.class, int.class);
105+
}
106+
return startActivityAsUserWithFeatureMethod;
107+
}
108+
109+
@SuppressWarnings("ConstantConditions")
110+
public int startActivityAsUserWithFeature(Intent intent) {
111+
try {
112+
Method method = getStartActivityAsUserWithFeatureMethod();
113+
return (int) method.invoke(
114+
/* this */ manager,
115+
/* caller */ null,
116+
/* callingPackage */ FakeContext.PACKAGE_NAME,
117+
/* callingFeatureId */ null,
118+
/* intent */ intent,
119+
/* resolvedType */ null,
120+
/* resultTo */ null,
121+
/* resultWho */ null,
122+
/* requestCode */ 0,
123+
/* startFlags */ 0,
124+
/* profilerInfo */ null,
125+
/* bOptions */ null,
126+
/* userId */ /* UserHandle.USER_CURRENT */ -2);
127+
} catch (Throwable e) {
128+
Ln.e("Could not invoke method", e);
129+
return 0;
130+
}
131+
}
132+
133+
private Method getForceStopPackageMethod() throws NoSuchMethodException {
134+
if (forceStopPackageMethod == null) {
135+
forceStopPackageMethod = manager.getClass().getMethod("forceStopPackage", String.class, int.class);
136+
}
137+
return forceStopPackageMethod;
138+
}
139+
140+
public void forceStopPackage(String packageName) {
141+
try {
142+
Method method = getForceStopPackageMethod();
143+
method.invoke(manager, packageName, /* userId */ /* UserHandle.USER_CURRENT */ -2);
144+
} catch (Throwable e) {
145+
Ln.e("Could not invoke method", e);
146+
}
147+
}
88148
}

0 commit comments

Comments
 (0)