Compare commits
15 Commits
android11_
...
android-fr
Author | SHA1 | Date | |
---|---|---|---|
f996386b6e | |||
cfc9882897 | |||
e4c152b1a3 | |||
6c5b20fdb1 | |||
512ef4e5c0 | |||
186a5fdcff | |||
fb3d09b7e3 | |||
ce3d7507ce | |||
2f9396e24a | |||
0ebb3df69c | |||
2fff9b9edf | |||
57f879d68a | |||
3626d90004 | |||
02f4ff7534 | |||
a3871130cc |
@ -277,10 +277,6 @@ if get_option('buildtype') == 'debug'
|
|||||||
'src/util/strbuf.c',
|
'src/util/strbuf.c',
|
||||||
'src/util/term.c',
|
'src/util/term.c',
|
||||||
]],
|
]],
|
||||||
['test_clock', [
|
|
||||||
'tests/test_clock.c',
|
|
||||||
'src/clock.c',
|
|
||||||
]],
|
|
||||||
['test_control_msg_serialize', [
|
['test_control_msg_serialize', [
|
||||||
'tests/test_control_msg_serialize.c',
|
'tests/test_control_msg_serialize.c',
|
||||||
'src/control_msg.c',
|
'src/control_msg.c',
|
||||||
@ -310,7 +306,8 @@ if get_option('buildtype') == 'debug'
|
|||||||
]
|
]
|
||||||
|
|
||||||
foreach t : tests
|
foreach t : tests
|
||||||
exe = executable(t[0], t[1],
|
sources = t[1] + ['src/compat.c']
|
||||||
|
exe = executable(t[0], sources,
|
||||||
include_directories: src_dir,
|
include_directories: src_dir,
|
||||||
dependencies: dependencies,
|
dependencies: dependencies,
|
||||||
c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST'])
|
c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST'])
|
||||||
|
108
app/src/clock.c
108
app/src/clock.c
@ -1,116 +1,36 @@
|
|||||||
#include "clock.h"
|
#include "clock.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
#define SC_CLOCK_NDEBUG // comment to debug
|
#define SC_CLOCK_NDEBUG // comment to debug
|
||||||
|
|
||||||
|
#define SC_CLOCK_RANGE 32
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_clock_init(struct sc_clock *clock) {
|
sc_clock_init(struct sc_clock *clock) {
|
||||||
clock->count = 0;
|
clock->range = 0;
|
||||||
clock->head = 0;
|
clock->offset = 0;
|
||||||
clock->left_sum.system = 0;
|
|
||||||
clock->left_sum.stream = 0;
|
|
||||||
clock->right_sum.system = 0;
|
|
||||||
clock->right_sum.stream = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Estimate the affine function f(stream) = slope * stream + offset
|
|
||||||
static void
|
|
||||||
sc_clock_estimate(struct sc_clock *clock,
|
|
||||||
double *out_slope, sc_tick *out_offset) {
|
|
||||||
assert(clock->count);
|
|
||||||
|
|
||||||
if (clock->count == 1) {
|
|
||||||
// If there is only 1 point, we can't compute a slope. Assume it is 1.
|
|
||||||
struct sc_clock_point *single_point = &clock->right_sum;
|
|
||||||
*out_slope = 1;
|
|
||||||
*out_offset = single_point->system - single_point->stream;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sc_clock_point left_avg = {
|
|
||||||
.system = clock->left_sum.system / (clock->count / 2),
|
|
||||||
.stream = clock->left_sum.stream / (clock->count / 2),
|
|
||||||
};
|
|
||||||
struct sc_clock_point right_avg = {
|
|
||||||
.system = clock->right_sum.system / ((clock->count + 1) / 2),
|
|
||||||
.stream = clock->right_sum.stream / ((clock->count + 1) / 2),
|
|
||||||
};
|
|
||||||
|
|
||||||
double slope = (double) (right_avg.system - left_avg.system)
|
|
||||||
/ (right_avg.stream - left_avg.stream);
|
|
||||||
|
|
||||||
if (clock->count < SC_CLOCK_RANGE) {
|
|
||||||
/* The first frames are typically received and decoded with more delay
|
|
||||||
* than the others, causing a wrong slope estimation on start. To
|
|
||||||
* compensate, assume an initial slope of 1, then progressively use the
|
|
||||||
* estimated slope. */
|
|
||||||
slope = (clock->count * slope + (SC_CLOCK_RANGE - clock->count))
|
|
||||||
/ SC_CLOCK_RANGE;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sc_clock_point global_avg = {
|
|
||||||
.system = (clock->left_sum.system + clock->right_sum.system)
|
|
||||||
/ clock->count,
|
|
||||||
.stream = (clock->left_sum.stream + clock->right_sum.stream)
|
|
||||||
/ clock->count,
|
|
||||||
};
|
|
||||||
|
|
||||||
sc_tick offset = global_avg.system - (sc_tick) (global_avg.stream * slope);
|
|
||||||
|
|
||||||
*out_slope = slope;
|
|
||||||
*out_offset = offset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
|
sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
|
||||||
struct sc_clock_point *point = &clock->points[clock->head];
|
if (clock->range < SC_CLOCK_RANGE) {
|
||||||
|
++clock->range;
|
||||||
if (clock->count == SC_CLOCK_RANGE || clock->count & 1) {
|
|
||||||
// One point passes from the right sum to the left sum
|
|
||||||
|
|
||||||
unsigned mid;
|
|
||||||
if (clock->count == SC_CLOCK_RANGE) {
|
|
||||||
mid = (clock->head + SC_CLOCK_RANGE / 2) % SC_CLOCK_RANGE;
|
|
||||||
} else {
|
|
||||||
// Only for the first frames
|
|
||||||
mid = clock->count / 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sc_clock_point *mid_point = &clock->points[mid];
|
sc_tick offset = system - stream;
|
||||||
clock->left_sum.system += mid_point->system;
|
clock->offset = ((clock->range - 1) * clock->offset + offset)
|
||||||
clock->left_sum.stream += mid_point->stream;
|
/ clock->range;
|
||||||
clock->right_sum.system -= mid_point->system;
|
|
||||||
clock->right_sum.stream -= mid_point->stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clock->count == SC_CLOCK_RANGE) {
|
|
||||||
// The current point overwrites the previous value in the circular
|
|
||||||
// array, update the left sum accordingly
|
|
||||||
clock->left_sum.system -= point->system;
|
|
||||||
clock->left_sum.stream -= point->stream;
|
|
||||||
} else {
|
|
||||||
++clock->count;
|
|
||||||
}
|
|
||||||
|
|
||||||
point->system = system;
|
|
||||||
point->stream = stream;
|
|
||||||
|
|
||||||
clock->right_sum.system += system;
|
|
||||||
clock->right_sum.stream += stream;
|
|
||||||
|
|
||||||
clock->head = (clock->head + 1) % SC_CLOCK_RANGE;
|
|
||||||
|
|
||||||
// Update estimation
|
|
||||||
sc_clock_estimate(clock, &clock->slope, &clock->offset);
|
|
||||||
|
|
||||||
#ifndef SC_CLOCK_NDEBUG
|
#ifndef SC_CLOCK_NDEBUG
|
||||||
LOGD("Clock estimation: %f * pts + %" PRItick, clock->slope, clock->offset);
|
LOGD("Clock estimation: pts + %" PRItick, clock->offset);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
sc_tick
|
sc_tick
|
||||||
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
|
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
|
||||||
assert(clock->count); // sc_clock_update() must have been called
|
assert(clock->range); // sc_clock_update() must have been called
|
||||||
return (sc_tick) (stream * clock->slope) + clock->offset;
|
return stream + clock->offset;
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,8 @@
|
|||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
|
|
||||||
#include "util/tick.h"
|
#include "util/tick.h"
|
||||||
|
|
||||||
#define SC_CLOCK_RANGE 32
|
|
||||||
static_assert(!(SC_CLOCK_RANGE & 1), "SC_CLOCK_RANGE must be even");
|
|
||||||
|
|
||||||
struct sc_clock_point {
|
struct sc_clock_point {
|
||||||
sc_tick system;
|
sc_tick system;
|
||||||
sc_tick stream;
|
sc_tick stream;
|
||||||
@ -21,40 +16,18 @@ struct sc_clock_point {
|
|||||||
*
|
*
|
||||||
* f(stream) = slope * stream + offset
|
* f(stream) = slope * stream + offset
|
||||||
*
|
*
|
||||||
* To that end, it stores the SC_CLOCK_RANGE last clock points (the timestamps
|
* Theoretically, the slope encodes the drift between the device clock and the
|
||||||
* of a frame expressed both in stream time and system time) in a circular
|
* computer clock. It is expected to be very close to 1.
|
||||||
* array.
|
|
||||||
*
|
*
|
||||||
* To estimate the slope, it splits the last SC_CLOCK_RANGE points into two
|
* Since the clock is used to estimate very close points in the future (which
|
||||||
* sets of SC_CLOCK_RANGE/2 points, and computes their centroid ("average
|
* are reestimated on every clock update, see delay_buffer), the error caused
|
||||||
* point"). The slope of the estimated affine function is that of the line
|
* by clock drift is totally negligible, so it is better to assume that the
|
||||||
* passing through these two points.
|
* slope is 1 than to estimate it (the estimation error would be larger).
|
||||||
*
|
*
|
||||||
* To estimate the offset, it computes the centroid of all the SC_CLOCK_RANGE
|
* Therefore, only the offset is estimated.
|
||||||
* points. The resulting affine function passes by this centroid.
|
|
||||||
*
|
|
||||||
* With a circular array, the rolling sums (and average) are quick to compute.
|
|
||||||
* In practice, the estimation is stable and the evolution is smooth.
|
|
||||||
*/
|
*/
|
||||||
struct sc_clock {
|
struct sc_clock {
|
||||||
// Circular array
|
unsigned range;
|
||||||
struct sc_clock_point points[SC_CLOCK_RANGE];
|
|
||||||
|
|
||||||
// Number of points in the array (count <= SC_CLOCK_RANGE)
|
|
||||||
unsigned count;
|
|
||||||
|
|
||||||
// Index of the next point to write
|
|
||||||
unsigned head;
|
|
||||||
|
|
||||||
// Sum of the first count/2 points
|
|
||||||
struct sc_clock_point left_sum;
|
|
||||||
|
|
||||||
// Sum of the last (count+1)/2 points
|
|
||||||
struct sc_clock_point right_sum;
|
|
||||||
|
|
||||||
// Estimated slope and offset
|
|
||||||
// (computed on sc_clock_update(), used by sc_clock_to_system_time())
|
|
||||||
double slope;
|
|
||||||
sc_tick offset;
|
sc_tick offset;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -194,7 +194,7 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink,
|
|||||||
sc_clock_update(&db->clock, sc_tick_now(), pts);
|
sc_clock_update(&db->clock, sc_tick_now(), pts);
|
||||||
sc_cond_signal(&db->wait_cond);
|
sc_cond_signal(&db->wait_cond);
|
||||||
|
|
||||||
if (db->first_frame_asap && db->clock.count == 1) {
|
if (db->first_frame_asap && db->clock.range == 1) {
|
||||||
sc_mutex_unlock(&db->mutex);
|
sc_mutex_unlock(&db->mutex);
|
||||||
return sc_frame_source_sinks_push(&db->frame_source, frame);
|
return sc_frame_source_sinks_push(&db->frame_source, frame);
|
||||||
}
|
}
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
|
|
||||||
#include "clock.h"
|
|
||||||
|
|
||||||
void test_small_rolling_sum(void) {
|
|
||||||
struct sc_clock clock;
|
|
||||||
sc_clock_init(&clock);
|
|
||||||
|
|
||||||
assert(clock.count == 0);
|
|
||||||
assert(clock.left_sum.system == 0);
|
|
||||||
assert(clock.left_sum.stream == 0);
|
|
||||||
assert(clock.right_sum.system == 0);
|
|
||||||
assert(clock.right_sum.stream == 0);
|
|
||||||
|
|
||||||
sc_clock_update(&clock, 2, 3);
|
|
||||||
assert(clock.count == 1);
|
|
||||||
assert(clock.left_sum.system == 0);
|
|
||||||
assert(clock.left_sum.stream == 0);
|
|
||||||
assert(clock.right_sum.system == 2);
|
|
||||||
assert(clock.right_sum.stream == 3);
|
|
||||||
|
|
||||||
sc_clock_update(&clock, 10, 20);
|
|
||||||
assert(clock.count == 2);
|
|
||||||
assert(clock.left_sum.system == 2);
|
|
||||||
assert(clock.left_sum.stream == 3);
|
|
||||||
assert(clock.right_sum.system == 10);
|
|
||||||
assert(clock.right_sum.stream == 20);
|
|
||||||
|
|
||||||
sc_clock_update(&clock, 40, 80);
|
|
||||||
assert(clock.count == 3);
|
|
||||||
assert(clock.left_sum.system == 2);
|
|
||||||
assert(clock.left_sum.stream == 3);
|
|
||||||
assert(clock.right_sum.system == 50);
|
|
||||||
assert(clock.right_sum.stream == 100);
|
|
||||||
|
|
||||||
sc_clock_update(&clock, 400, 800);
|
|
||||||
assert(clock.count == 4);
|
|
||||||
assert(clock.left_sum.system == 12);
|
|
||||||
assert(clock.left_sum.stream == 23);
|
|
||||||
assert(clock.right_sum.system == 440);
|
|
||||||
assert(clock.right_sum.stream == 880);
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_large_rolling_sum(void) {
|
|
||||||
const unsigned half_range = SC_CLOCK_RANGE / 2;
|
|
||||||
|
|
||||||
struct sc_clock clock1;
|
|
||||||
sc_clock_init(&clock1);
|
|
||||||
for (unsigned i = 0; i < 5 * half_range; ++i) {
|
|
||||||
sc_clock_update(&clock1, i, 2 * i + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sc_clock clock2;
|
|
||||||
sc_clock_init(&clock2);
|
|
||||||
for (unsigned i = 3 * half_range; i < 5 * half_range; ++i) {
|
|
||||||
sc_clock_update(&clock2, i, 2 * i + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(clock1.count == SC_CLOCK_RANGE);
|
|
||||||
assert(clock2.count == SC_CLOCK_RANGE);
|
|
||||||
|
|
||||||
// The values before the last SC_CLOCK_RANGE points in clock1 should have
|
|
||||||
// no impact
|
|
||||||
assert(clock1.left_sum.system == clock2.left_sum.system);
|
|
||||||
assert(clock1.left_sum.stream == clock2.left_sum.stream);
|
|
||||||
assert(clock1.right_sum.system == clock2.right_sum.system);
|
|
||||||
assert(clock1.right_sum.stream == clock2.right_sum.stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
(void) argc;
|
|
||||||
(void) argv;
|
|
||||||
|
|
||||||
test_small_rolling_sum();
|
|
||||||
test_large_rolling_sum();
|
|
||||||
return 0;
|
|
||||||
};
|
|
@ -14,8 +14,8 @@ set -e
|
|||||||
SCRCPY_DEBUG=false
|
SCRCPY_DEBUG=false
|
||||||
SCRCPY_VERSION_NAME=2.0
|
SCRCPY_VERSION_NAME=2.0
|
||||||
|
|
||||||
PLATFORM=${ANDROID_PLATFORM:-33}
|
PLATFORM=${ANDROID_PLATFORM:-23}
|
||||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0}
|
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-23.0.3}
|
||||||
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
|
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
|
||||||
|
|
||||||
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
|
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
|
||||||
@ -43,6 +43,17 @@ public final class BuildConfig {
|
|||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
STUBS_DIR="$BUILD_DIR/stubs"
|
||||||
|
rm -rf "$STUBS_DIR"
|
||||||
|
mkdir -p "$STUBS_DIR"
|
||||||
|
echo "Generating SDK stubs..."
|
||||||
|
cd "$SERVER_DIR/src/main/stubs"
|
||||||
|
javac -bootclasspath "$ANDROID_JAR" \
|
||||||
|
-d "$STUBS_DIR" \
|
||||||
|
-source 1.8 -target 1.8 \
|
||||||
|
android/content/*
|
||||||
|
cd -
|
||||||
|
|
||||||
echo "Generating java from aidl..."
|
echo "Generating java from aidl..."
|
||||||
cd "$SERVER_DIR/src/main/aidl"
|
cd "$SERVER_DIR/src/main/aidl"
|
||||||
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl
|
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl
|
||||||
@ -52,7 +63,7 @@ cd "$SERVER_DIR/src/main/aidl"
|
|||||||
echo "Compiling java sources..."
|
echo "Compiling java sources..."
|
||||||
cd ../java
|
cd ../java
|
||||||
javac -bootclasspath "$ANDROID_JAR" \
|
javac -bootclasspath "$ANDROID_JAR" \
|
||||||
-cp "$LAMBDA_JAR:$GEN_DIR" \
|
-cp "$LAMBDA_JAR:$GEN_DIR:$STUBS_DIR" \
|
||||||
-d "$CLASSES_DIR" \
|
-d "$CLASSES_DIR" \
|
||||||
-source 1.8 -target 1.8 \
|
-source 1.8 -target 1.8 \
|
||||||
com/genymobile/scrcpy/*.java \
|
com/genymobile/scrcpy/*.java \
|
||||||
|
@ -5,6 +5,7 @@ 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.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioRecord;
|
import android.media.AudioRecord;
|
||||||
@ -14,6 +15,7 @@ import android.media.MediaRecorder;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
public final class AudioCapture {
|
public final class AudioCapture {
|
||||||
@ -42,13 +44,28 @@ public final class AudioCapture {
|
|||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Method setBuilderContext;
|
||||||
|
|
||||||
|
@TargetApi(23)
|
||||||
|
private static void setBuilderContext(AudioRecord.Builder builder, Context context) {
|
||||||
|
try {
|
||||||
|
if (setBuilderContext == null) {
|
||||||
|
setBuilderContext = AudioRecord.Builder.class.getMethod("setContext", Context.class);
|
||||||
|
}
|
||||||
|
setBuilderContext.invoke(builder, context);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Ln.e("Could not call AudioRecord.Builder.setContext() method");
|
||||||
|
//throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.M)
|
@TargetApi(Build.VERSION_CODES.M)
|
||||||
@SuppressLint({"WrongConstant", "MissingPermission"})
|
@SuppressLint({"WrongConstant", "MissingPermission"})
|
||||||
private static AudioRecord createAudioRecord() {
|
private static AudioRecord createAudioRecord() {
|
||||||
AudioRecord.Builder builder = new AudioRecord.Builder();
|
AudioRecord.Builder builder = new AudioRecord.Builder();
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
|
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
|
||||||
builder.setContext(FakeContext.get());
|
setBuilderContext(builder, FakeContext.get());
|
||||||
}
|
}
|
||||||
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
|
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
|
||||||
builder.setAudioFormat(createAudioFormat());
|
builder.setAudioFormat(createAudioFormat());
|
||||||
@ -59,46 +76,59 @@ public final class AudioCapture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void startWorkaroundAndroid11() {
|
private static void startWorkaroundAndroid11() {
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
|
||||||
// Android 11 requires Apps to be at foreground to record audio.
|
// 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.
|
// 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
|
// 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").
|
// 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
|
// If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the
|
||||||
// foreground.
|
// foreground.
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||||
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
|
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
|
||||||
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
|
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
|
||||||
// Wait for activity to start
|
|
||||||
SystemClock.sleep(150);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void stopWorkaroundAndroid11() {
|
private static void stopWorkaroundAndroid11() {
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
|
||||||
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
|
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureForegroundException {
|
||||||
|
while (attempts-- > 0) {
|
||||||
|
// Wait for activity to start
|
||||||
|
SystemClock.sleep(delayMs);
|
||||||
|
try {
|
||||||
|
startRecording();
|
||||||
|
return; // it worked
|
||||||
|
} catch (UnsupportedOperationException e) {
|
||||||
|
if (attempts == 0) {
|
||||||
|
Ln.e("Failed to start audio capture");
|
||||||
|
Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " +
|
||||||
|
"scrcpy.");
|
||||||
|
throw new AudioCaptureForegroundException();
|
||||||
|
} else {
|
||||||
|
Ln.d("Failed to start audio capture, retrying...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() throws AudioCaptureForegroundException {
|
private void startRecording() {
|
||||||
startWorkaroundAndroid11();
|
|
||||||
try {
|
|
||||||
recorder = createAudioRecord();
|
recorder = createAudioRecord();
|
||||||
recorder.startRecording();
|
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;
|
|
||||||
|
public void start() throws AudioCaptureForegroundException {
|
||||||
|
if (Build.VERSION.SDK_INT == 30) {
|
||||||
|
startWorkaroundAndroid11();
|
||||||
|
try {
|
||||||
|
tryStartRecording(3, 100);
|
||||||
} finally {
|
} finally {
|
||||||
stopWorkaroundAndroid11();
|
stopWorkaroundAndroid11();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
startRecording();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
@ -108,7 +138,21 @@ public final class AudioCapture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
private static Method getTimestampMethod;
|
||||||
|
|
||||||
|
private static int getRecorderTimestamp(AudioRecord recorder, AudioTimestamp timestamp) {
|
||||||
|
try {
|
||||||
|
if (getTimestampMethod == null) {
|
||||||
|
getTimestampMethod = AudioRecord.class.getMethod("getTimestamp", AudioTimestamp.class, int.class);
|
||||||
|
}
|
||||||
|
return (int) getTimestampMethod.invoke(recorder, timestamp, 0);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Ln.e("Could not call AudioRecord.getTimestamp() method");
|
||||||
|
return AudioRecord.ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(24)
|
||||||
public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) {
|
public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) {
|
||||||
int r = recorder.read(directBuffer, size);
|
int r = recorder.read(directBuffer, size);
|
||||||
if (r <= 0) {
|
if (r <= 0) {
|
||||||
@ -117,7 +161,7 @@ public final class AudioCapture {
|
|||||||
|
|
||||||
long pts;
|
long pts;
|
||||||
|
|
||||||
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
|
int ret = getRecorderTimestamp(recorder, timestamp);
|
||||||
if (ret == AudioRecord.SUCCESS) {
|
if (ret == AudioRecord.SUCCESS) {
|
||||||
pts = timestamp.nanoTime / 1000;
|
pts = timestamp.nanoTime / 1000;
|
||||||
} else {
|
} else {
|
||||||
|
@ -84,7 +84,7 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
@TargetApi(24)
|
||||||
private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException {
|
private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException {
|
||||||
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||||
|
|
||||||
@ -159,7 +159,7 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.M)
|
@TargetApi(Build.VERSION_CODES.M)
|
||||||
public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException {
|
public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT < 30) {
|
||||||
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);
|
||||||
return;
|
return;
|
||||||
@ -271,17 +271,26 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
try {
|
try {
|
||||||
return MediaCodec.createByCodecName(encoderName);
|
return MediaCodec.createByCodecName(encoderName);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
|
Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
|
||||||
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Ln.e("Could not create audio encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage());
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||||
Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
|
Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
|
||||||
return mediaCodec;
|
return mediaCodec;
|
||||||
|
} catch (IOException | IllegalArgumentException e) {
|
||||||
|
Ln.e("Could not create default audio encoder for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class EncoderCallback extends MediaCodec.Callback {
|
private class EncoderCallback extends MediaCodec.Callback {
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
@TargetApi(24)
|
||||||
@Override
|
@Override
|
||||||
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
||||||
try {
|
try {
|
||||||
|
@ -373,8 +373,8 @@ public class Controller implements AsyncProcessor {
|
|||||||
|
|
||||||
private void getClipboard(int copyKey) {
|
private void getClipboard(int copyKey) {
|
||||||
// On Android >= 7, press the COPY or CUT key if requested
|
// On Android >= 7, press the COPY or CUT key if requested
|
||||||
if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
|
if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= 24 && device.supportsInputEvents()) {
|
||||||
int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT;
|
int key = copyKey == ControlMessage.COPY_KEY_COPY ? 278 : 277;
|
||||||
// Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one
|
// Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one
|
||||||
device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH);
|
device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH);
|
||||||
}
|
}
|
||||||
@ -397,8 +397,8 @@ public class Controller implements AsyncProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// On Android >= 7, also press the PASTE key if requested
|
// On Android >= 7, also press the PASTE key if requested
|
||||||
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
|
if (paste && Build.VERSION.SDK_INT >= 24 && device.supportsInputEvents()) {
|
||||||
device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC);
|
device.pressReleaseKeycode(279, Device.INJECT_MODE_ASYNC);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sequence != ControlMessage.SEQUENCE_INVALID) {
|
if (sequence != ControlMessage.SEQUENCE_INVALID) {
|
||||||
|
@ -68,7 +68,8 @@ public final class DesktopConnection implements Closeable {
|
|||||||
LocalSocket controlSocket = null;
|
LocalSocket controlSocket = null;
|
||||||
try {
|
try {
|
||||||
if (tunnelForward) {
|
if (tunnelForward) {
|
||||||
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
|
LocalServerSocket localServerSocket = new LocalServerSocket(socketName);
|
||||||
|
try {
|
||||||
videoSocket = localServerSocket.accept();
|
videoSocket = localServerSocket.accept();
|
||||||
if (sendDummyByte) {
|
if (sendDummyByte) {
|
||||||
// send one byte so the client may read() to detect a connection error
|
// send one byte so the client may read() to detect a connection error
|
||||||
@ -80,6 +81,8 @@ public final class DesktopConnection implements Closeable {
|
|||||||
if (control) {
|
if (control) {
|
||||||
controlSocket = localServerSocket.accept();
|
controlSocket = localServerSocket.accept();
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
localServerSocket.close();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
videoSocket = connect(socketName);
|
videoSocket = connect(socketName);
|
||||||
|
@ -124,7 +124,7 @@ public final class Device {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// main display or any display on Android >= Q
|
// main display or any display on Android >= Q
|
||||||
supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
|
supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= 29;
|
||||||
if (!supportsInputEvents) {
|
if (!supportsInputEvents) {
|
||||||
Ln.w("Input events are not supported for secondary displays before Android 10");
|
Ln.w("Input events are not supported for secondary displays before Android 10");
|
||||||
}
|
}
|
||||||
@ -173,7 +173,7 @@ public final class Device {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean supportsInputEvents(int displayId) {
|
public static boolean supportsInputEvents(int displayId) {
|
||||||
return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
|
return displayId == 0 || Build.VERSION.SDK_INT >= 29;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean supportsInputEvents() {
|
public boolean supportsInputEvents() {
|
||||||
@ -277,7 +277,7 @@ public final class Device {
|
|||||||
* @param mode one of the {@code POWER_MODE_*} constants
|
* @param mode one of the {@code POWER_MODE_*} constants
|
||||||
*/
|
*/
|
||||||
public static boolean setScreenPowerMode(int mode) {
|
public static boolean setScreenPowerMode(int mode) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= 29) {
|
||||||
// Change the power mode for all physical displays
|
// Change the power mode for all physical displays
|
||||||
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
|
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
|
||||||
if (physicalDisplayIds == null) {
|
if (physicalDisplayIds == null) {
|
||||||
|
@ -26,16 +26,20 @@ public final class FakeContext extends ContextWrapper {
|
|||||||
return PACKAGE_NAME;
|
return PACKAGE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getOpPackageName() {
|
public String getOpPackageName() {
|
||||||
return PACKAGE_NAME;
|
return PACKAGE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.S)
|
@TargetApi(31)
|
||||||
@Override
|
|
||||||
public AttributionSource getAttributionSource() {
|
public AttributionSource getAttributionSource() {
|
||||||
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
|
AttributionSource.Builder builder = new AttributionSource.Builder(0);
|
||||||
builder.setPackageName(PACKAGE_NAME);
|
builder.setPackageName(PACKAGE_NAME);
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Override to be added on SDK upgrade for Android 14
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public int getDeviceId() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,13 +202,22 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
try {
|
try {
|
||||||
return MediaCodec.createByCodecName(encoderName);
|
return MediaCodec.createByCodecName(encoderName);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
|
Ln.e("Video encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
|
||||||
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Ln.e("Could not create video encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage());
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||||
Ln.d("Using encoder: '" + mediaCodec.getName() + "'");
|
Ln.d("Using video encoder: '" + mediaCodec.getName() + "'");
|
||||||
return mediaCodec;
|
return mediaCodec;
|
||||||
|
} catch (IOException | IllegalArgumentException e) {
|
||||||
|
Ln.e("Could not create default video encoder for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||||
@ -243,7 +252,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
private static IBinder createDisplay() {
|
private static IBinder createDisplay() {
|
||||||
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore.
|
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore.
|
||||||
// On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S".
|
// On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S".
|
||||||
boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S"
|
boolean secure = Build.VERSION.SDK_INT < 30 || (Build.VERSION.SDK_INT == 30 && !"S"
|
||||||
.equals(Build.VERSION.CODENAME));
|
.equals(Build.VERSION.CODENAME));
|
||||||
return SurfaceControl.createDisplay("scrcpy", secure);
|
return SurfaceControl.createDisplay("scrcpy", secure);
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ public final class Server {
|
|||||||
// Before Android 11, audio is not supported.
|
// Before Android 11, audio is not supported.
|
||||||
// Since Android 12, we can properly set a context on the AudioRecord.
|
// Since Android 12, we can properly set a context on the AudioRecord.
|
||||||
// Only on Android 11 we must fill the application context for the AudioRecord to work.
|
// Only on Android 11 we must fill the application context for the AudioRecord to work.
|
||||||
if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
if (audio && Build.VERSION.SDK_INT == 30) {
|
||||||
Workarounds.fillAppContext();
|
Workarounds.fillAppContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ public final class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getValue(String table, String key) throws SettingsException {
|
public static String getValue(String table, String key) throws SettingsException {
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT <= 30) {
|
||||||
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||||
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
|
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
|
||||||
return provider.getValue(table, key);
|
return provider.getValue(table, key);
|
||||||
@ -47,7 +47,7 @@ public final class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void putValue(String table, String key, String value) throws SettingsException {
|
public static void putValue(String table, String key, String value) throws SettingsException {
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT <= 30) {
|
||||||
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||||
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
|
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
|
||||||
provider.putValue(table, key, value);
|
provider.putValue(table, key, value);
|
||||||
@ -60,7 +60,7 @@ public final class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getAndPutValue(String table, String key, String value) throws SettingsException {
|
public static String getAndPutValue(String table, String key, String value) throws SettingsException {
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT <= 30) {
|
||||||
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||||
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
|
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
|
||||||
String oldValue = provider.getValue(table, key);
|
String oldValue = provider.getValue(table, key);
|
||||||
|
@ -7,7 +7,7 @@ public enum VideoCodec implements Codec {
|
|||||||
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
|
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
|
||||||
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
|
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
|
||||||
@SuppressLint("InlinedApi") // introduced in API 21
|
@SuppressLint("InlinedApi") // introduced in API 21
|
||||||
AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1);
|
AV1(0x00_61_76_31, "av1", "video/av01");
|
||||||
|
|
||||||
private final int id; // 4-byte ASCII representation of the name
|
private final int id; // 4-byte ASCII representation of the name
|
||||||
private final String name;
|
private final String name;
|
||||||
|
@ -51,7 +51,7 @@ public class ActivityManager {
|
|||||||
return removeContentProviderExternalMethod;
|
return removeContentProviderExternalMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.Q)
|
@TargetApi(29)
|
||||||
private ContentProvider getContentProviderExternal(String name, IBinder token) {
|
private ContentProvider getContentProviderExternal(String name, IBinder token) {
|
||||||
try {
|
try {
|
||||||
Method method = getGetContentProviderExternalMethod();
|
Method method = getGetContentProviderExternalMethod();
|
||||||
|
@ -16,9 +16,9 @@ public class ClipboardManager {
|
|||||||
private Method getPrimaryClipMethod;
|
private Method getPrimaryClipMethod;
|
||||||
private Method setPrimaryClipMethod;
|
private Method setPrimaryClipMethod;
|
||||||
private Method addPrimaryClipChangedListener;
|
private Method addPrimaryClipChangedListener;
|
||||||
private boolean alternativeGetMethod;
|
private int getMethodVersion;
|
||||||
private boolean alternativeSetMethod;
|
private int setMethodVersion;
|
||||||
private boolean alternativeAddListenerMethod;
|
private int addListenerMethodVersion;
|
||||||
|
|
||||||
public ClipboardManager(IInterface manager) {
|
public ClipboardManager(IInterface manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@ -26,14 +26,20 @@ public class ClipboardManager {
|
|||||||
|
|
||||||
private Method getGetPrimaryClipMethod() throws NoSuchMethodException {
|
private Method getGetPrimaryClipMethod() throws NoSuchMethodException {
|
||||||
if (getPrimaryClipMethod == null) {
|
if (getPrimaryClipMethod == null) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < 29) {
|
||||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
|
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
|
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
|
||||||
} catch (NoSuchMethodException e) {
|
getMethodVersion = 0;
|
||||||
|
} catch (NoSuchMethodException e1) {
|
||||||
|
try {
|
||||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
|
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
|
||||||
alternativeGetMethod = true;
|
getMethodVersion = 1;
|
||||||
|
} catch (NoSuchMethodException e2) {
|
||||||
|
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
|
||||||
|
getMethodVersion = 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,46 +48,67 @@ public class ClipboardManager {
|
|||||||
|
|
||||||
private Method getSetPrimaryClipMethod() throws NoSuchMethodException {
|
private Method getSetPrimaryClipMethod() throws NoSuchMethodException {
|
||||||
if (setPrimaryClipMethod == null) {
|
if (setPrimaryClipMethod == null) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < 29) {
|
||||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
|
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
|
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
|
||||||
} catch (NoSuchMethodException e) {
|
setMethodVersion = 0;
|
||||||
|
} catch (NoSuchMethodException e1) {
|
||||||
|
try {
|
||||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
|
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
|
||||||
alternativeSetMethod = true;
|
setMethodVersion = 1;
|
||||||
|
} catch (NoSuchMethodException e2) {
|
||||||
|
setPrimaryClipMethod = manager.getClass()
|
||||||
|
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class);
|
||||||
|
setMethodVersion = 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return setPrimaryClipMethod;
|
return setPrimaryClipMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager)
|
private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager)
|
||||||
throws InvocationTargetException, IllegalAccessException {
|
throws InvocationTargetException, IllegalAccessException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < 29) {
|
||||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
|
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
|
||||||
}
|
}
|
||||||
if (alternativeMethod) {
|
|
||||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
switch (methodVersion) {
|
||||||
}
|
case 0:
|
||||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||||
|
case 1:
|
||||||
|
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||||
|
default:
|
||||||
|
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData)
|
private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData)
|
||||||
throws InvocationTargetException, IllegalAccessException {
|
throws InvocationTargetException, IllegalAccessException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < 29) {
|
||||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
|
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
|
||||||
} else if (alternativeMethod) {
|
return;
|
||||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
}
|
||||||
} else {
|
|
||||||
|
switch (methodVersion) {
|
||||||
|
case 0:
|
||||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public CharSequence getText() {
|
public CharSequence getText() {
|
||||||
try {
|
try {
|
||||||
Method method = getGetPrimaryClipMethod();
|
Method method = getGetPrimaryClipMethod();
|
||||||
ClipData clipData = getPrimaryClip(method, alternativeGetMethod, manager);
|
ClipData clipData = getPrimaryClip(method, getMethodVersion, manager);
|
||||||
if (clipData == null || clipData.getItemCount() == 0) {
|
if (clipData == null || clipData.getItemCount() == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -96,7 +123,7 @@ public class ClipboardManager {
|
|||||||
try {
|
try {
|
||||||
Method method = getSetPrimaryClipMethod();
|
Method method = getSetPrimaryClipMethod();
|
||||||
ClipData clipData = ClipData.newPlainText(null, text);
|
ClipData clipData = ClipData.newPlainText(null, text);
|
||||||
setPrimaryClip(method, alternativeSetMethod, manager, clipData);
|
setPrimaryClip(method, setMethodVersion, manager, clipData);
|
||||||
return true;
|
return true;
|
||||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
Ln.e("Could not invoke method", e);
|
Ln.e("Could not invoke method", e);
|
||||||
@ -104,30 +131,48 @@ public class ClipboardManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager,
|
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager,
|
||||||
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
|
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < 29) {
|
||||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
|
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
|
||||||
} else if (alternativeMethod) {
|
return;
|
||||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
}
|
||||||
} else {
|
|
||||||
|
switch (methodVersion) {
|
||||||
|
case 0:
|
||||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
|
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
|
||||||
if (addPrimaryClipChangedListener == null) {
|
if (addPrimaryClipChangedListener == null) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < 29) {
|
||||||
addPrimaryClipChangedListener = manager.getClass()
|
addPrimaryClipChangedListener = manager.getClass()
|
||||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
|
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
addPrimaryClipChangedListener = manager.getClass()
|
addPrimaryClipChangedListener = manager.getClass()
|
||||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
|
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
|
||||||
} catch (NoSuchMethodException e) {
|
addListenerMethodVersion = 0;
|
||||||
|
} catch (NoSuchMethodException e1) {
|
||||||
|
try {
|
||||||
addPrimaryClipChangedListener = manager.getClass()
|
addPrimaryClipChangedListener = manager.getClass()
|
||||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, int.class);
|
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
|
||||||
alternativeAddListenerMethod = true;
|
int.class);
|
||||||
|
addListenerMethodVersion = 1;
|
||||||
|
} catch (NoSuchMethodException e2) {
|
||||||
|
addPrimaryClipChangedListener = manager.getClass()
|
||||||
|
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
|
||||||
|
int.class, int.class);
|
||||||
|
addListenerMethodVersion = 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,7 +182,7 @@ public class ClipboardManager {
|
|||||||
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
|
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
|
||||||
try {
|
try {
|
||||||
Method method = getAddPrimaryClipChangedListener();
|
Method method = getAddPrimaryClipChangedListener();
|
||||||
addPrimaryClipChangedListener(method, alternativeAddListenerMethod, manager, listener);
|
addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener);
|
||||||
return true;
|
return true;
|
||||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
Ln.e("Could not invoke method", e);
|
Ln.e("Could not invoke method", e);
|
||||||
|
@ -54,7 +54,7 @@ public class ContentProvider implements Closeable {
|
|||||||
@SuppressLint("PrivateApi")
|
@SuppressLint("PrivateApi")
|
||||||
private Method getCallMethod() throws NoSuchMethodException {
|
private Method getCallMethod() throws NoSuchMethodException {
|
||||||
if (callMethod == null) {
|
if (callMethod == null) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class);
|
callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class);
|
||||||
callMethodVersion = 0;
|
callMethodVersion = 0;
|
||||||
} else {
|
} else {
|
||||||
@ -83,7 +83,7 @@ public class ContentProvider implements Closeable {
|
|||||||
Method method = getCallMethod();
|
Method method = getCallMethod();
|
||||||
Object[] args;
|
Object[] args;
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) {
|
if (Build.VERSION.SDK_INT >= 31 && callMethodVersion == 0) {
|
||||||
args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras};
|
args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras};
|
||||||
} else {
|
} else {
|
||||||
switch (callMethodVersion) {
|
switch (callMethodVersion) {
|
||||||
|
@ -90,7 +90,7 @@ public final class SurfaceControl {
|
|||||||
if (getBuiltInDisplayMethod == null) {
|
if (getBuiltInDisplayMethod == null) {
|
||||||
// the method signature has changed in Android Q
|
// the method signature has changed in Android Q
|
||||||
// <https://github.com/Genymobile/scrcpy/issues/586>
|
// <https://github.com/Genymobile/scrcpy/issues/586>
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < 29) {
|
||||||
getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class);
|
getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class);
|
||||||
} else {
|
} else {
|
||||||
getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken");
|
getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken");
|
||||||
@ -102,7 +102,7 @@ public final class SurfaceControl {
|
|||||||
public static IBinder getBuiltInDisplay() {
|
public static IBinder getBuiltInDisplay() {
|
||||||
try {
|
try {
|
||||||
Method method = getGetBuiltInDisplayMethod();
|
Method method = getGetBuiltInDisplayMethod();
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < 29) {
|
||||||
// call getBuiltInDisplay(0)
|
// call getBuiltInDisplay(0)
|
||||||
return (IBinder) method.invoke(null, 0);
|
return (IBinder) method.invoke(null, 0);
|
||||||
}
|
}
|
||||||
|
15
server/src/main/stubs/android/content/AttributionSource.java
Normal file
15
server/src/main/stubs/android/content/AttributionSource.java
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package android.content;
|
||||||
|
|
||||||
|
public class AttributionSource {
|
||||||
|
public static class Builder {
|
||||||
|
public Builder(int uid) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
public Builder setPackageName(String value) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
public AttributionSource build() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user