diff --git a/README.md b/README.md index db531c6100..7f6af3dd53 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Its features include: ## Requirements -The Android device requires at least API 21 (Android 5.0). +The Android device requires at least API 19 (Android 4.4). Make sure you [enable adb debugging][enable-adb] on your device(s). diff --git a/app/src/server.c b/app/src/server.c index 663ef18bb4..ba00b14f6c 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -18,6 +18,7 @@ #define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME #define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" +#define SC_DEVICE_ANDROID_DATA_PATH "/data/local/tmp" static char * get_server_path(void) { @@ -166,6 +167,9 @@ execute_server(struct sc_server *server, cmd[count++] = "-s"; cmd[count++] = serial; cmd[count++] = "shell"; + cmd[count++] = "mkdir -p " SC_DEVICE_ANDROID_DATA_PATH "/dalvik-cache"; + cmd[count++] = "&&"; + cmd[count++] = "ANDROID_DATA=" SC_DEVICE_ANDROID_DATA_PATH; cmd[count++] = "CLASSPATH=" SC_DEVICE_SERVER_PATH; cmd[count++] = "app_process"; diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index edda3919af..ca0d813b4a 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -152,7 +152,6 @@ page at http://checkstyle.sourceforge.net/config.html --> - diff --git a/gradle.properties b/gradle.properties index 89196d138d..01236edf1c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,3 +16,4 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true +android.useAndroidX=true diff --git a/libcore/build.gradle b/libcore/build.gradle new file mode 100644 index 0000000000..9900fd60df --- /dev/null +++ b/libcore/build.gradle @@ -0,0 +1 @@ +apply plugin: 'java-library' diff --git a/libcore/src/main/java/libcore/io/ErrnoException.java b/libcore/src/main/java/libcore/io/ErrnoException.java new file mode 100644 index 0000000000..be89321a26 --- /dev/null +++ b/libcore/src/main/java/libcore/io/ErrnoException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011 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 libcore.io; + +/** + * A checked exception thrown when {@link Os} methods fail. This exception contains the native + * errno value, for comparison against the constants in {@link OsConstants}, should sophisticated + * callers need to adjust their behavior based on the exact failure. + */ +public final class ErrnoException extends Exception { + private final String functionName; + public final int errno; + + public ErrnoException(String functionName, int errno) { + throw new AssertionError(); + } +} diff --git a/libcore/src/main/java/libcore/io/Libcore.java b/libcore/src/main/java/libcore/io/Libcore.java new file mode 100644 index 0000000000..0231ecb5c6 --- /dev/null +++ b/libcore/src/main/java/libcore/io/Libcore.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2011 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 libcore.io; + +public final class Libcore { + private Libcore() { + } + + public static Os os = null; +} diff --git a/libcore/src/main/java/libcore/io/Os.java b/libcore/src/main/java/libcore/io/Os.java new file mode 100644 index 0000000000..74f08ff902 --- /dev/null +++ b/libcore/src/main/java/libcore/io/Os.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2011 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 libcore.io; + +import java.io.FileDescriptor; +import java.nio.ByteBuffer; + +public interface Os { + String strerror(int errno); + int write(FileDescriptor fd, ByteBuffer buffer) throws ErrnoException; +} diff --git a/libcore/src/main/java/libcore/io/OsConstants.java b/libcore/src/main/java/libcore/io/OsConstants.java new file mode 100644 index 0000000000..ea0af53bc4 --- /dev/null +++ b/libcore/src/main/java/libcore/io/OsConstants.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011 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 libcore.io; + +public final class OsConstants { + private OsConstants() { + } + + public static final int EINTR = placeholder(); + + public static String errnoName(int errno) { + return null; + } + + // A hack to avoid these constants being inlined by javac... + private static int placeholder() { return 0; } +} diff --git a/os-compat/build.gradle b/os-compat/build.gradle new file mode 100644 index 0000000000..e85d6cfb81 --- /dev/null +++ b/os-compat/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 31 + defaultConfig { + minSdkVersion 19 + targetSdkVersion 31 + } + androidResources { + namespace = "androidx.system" + } + buildFeatures { + aidl = false + androidResources = false + buildConfig = false + } +} + +dependencies { + compileOnly rootProject.fileTree("thirdparty/androidx/annotation/1.3.0/annotation-1.3.0.jar") + compileOnly project(':libcore') +} + +apply from: "$project.rootDir/config/android-checkstyle.gradle" diff --git a/os-compat/src/main/java/androidx/system/ErrnoException.java b/os-compat/src/main/java/androidx/system/ErrnoException.java new file mode 100644 index 0000000000..b39d159bb5 --- /dev/null +++ b/os-compat/src/main/java/androidx/system/ErrnoException.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2011 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.system; + +import java.io.IOException; +import java.net.SocketException; + +/** + * A checked exception thrown when {@link OsCompat} methods fail. This exception contains the native + * errno value, for comparison against the constants in {@link OsConstantsCompat}, should sophisticated + * callers need to adjust their behavior based on the exact failure. + */ +public final class ErrnoException extends Exception { + private final String functionName; + private final int errno; + + /** + * Constructs an instance with the given function name and errno value. + */ + public ErrnoException(String functionName, int errno) { + this.functionName = functionName; + this.errno = errno; + } + + /** + * Constructs an instance with the given function name, errno value, and cause. + */ + public ErrnoException(String functionName, int errno, Throwable cause) { + super(cause); + this.functionName = functionName; + this.errno = errno; + } + + /** + * The errno value, for comparison with the {@code E} constants in {@link OsConstantsCompat}. + */ + public int getErrno() { + return errno; + } + + /** + * Converts the stashed function name and errno value to a human-readable string. + * We do this here rather than in the constructor so that callers only pay for + * this if they need it. + */ + @Override public String getMessage() { + String errnoName = OsConstantsCompat.errnoName(errno); + if (errnoName == null) { + errnoName = "errno " + errno; + } + String description = OsCompat.strerror(errno); + return functionName + " failed: " + errnoName + " (" + description + ")"; + } + + /** + * Throws an {@link IOException} with a message based on {@link #getMessage()} and with this + * instance as the cause. + * + *

This method always terminates by throwing the exception. Callers can write + * {@code throw e.rethrowAsIOException()} to make that clear to the compiler. + */ + public IOException rethrowAsIOException() throws IOException { + throw new IOException(getMessage(), this); + } + + /** + * Throws a {@link SocketException} with a message based on {@link #getMessage()} and with this + * instance as the cause. + * + *

This method always terminates by throwing the exception. Callers can write + * {@code throw e.rethrowAsIOException()} to make that clear to the compiler. + */ + public SocketException rethrowAsSocketException() throws SocketException { + final SocketException newException = new SocketException(getMessage()); + newException.initCause(this); + throw newException; + } +} diff --git a/os-compat/src/main/java/androidx/system/Os.java b/os-compat/src/main/java/androidx/system/Os.java new file mode 100644 index 0000000000..32d6b495d6 --- /dev/null +++ b/os-compat/src/main/java/androidx/system/Os.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2011 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.system; + +import java.io.FileDescriptor; +import java.io.InterruptedIOException; +import java.nio.ByteBuffer; + +interface Os { + String strerror(int errno); + + int write(FileDescriptor fd, ByteBuffer buffer) throws ErrnoException, InterruptedIOException; + + // https://android.googlesource.com/platform/libcore/+/lollipop-mr1-release/luni/src/main/java/libcore/io/Posix.java#253 + static void maybeUpdateBufferPosition(ByteBuffer buffer, int originalPosition, int bytesReadOrWritten) { + if (bytesReadOrWritten > 0) { + buffer.position(bytesReadOrWritten + originalPosition); + } + } +} diff --git a/os-compat/src/main/java/androidx/system/OsApi21.java b/os-compat/src/main/java/androidx/system/OsApi21.java new file mode 100644 index 0000000000..973aa4df5c --- /dev/null +++ b/os-compat/src/main/java/androidx/system/OsApi21.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011 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.system; + +import androidx.annotation.RequiresApi; + +import java.io.FileDescriptor; +import java.io.InterruptedIOException; +import java.nio.ByteBuffer; + +import static android.os.Build.VERSION.SDK_INT; +import static androidx.system.Os.maybeUpdateBufferPosition; + +@RequiresApi(21) +final class OsApi21 implements Os { + @Override + public String strerror(int errno) { + return android.system.Os.strerror(errno); + } + + @Override + public int write(FileDescriptor fd, ByteBuffer buffer) throws ErrnoException, InterruptedIOException { + try { + final int position = buffer.position(); + final int bytesWritten = android.system.Os.write(fd, buffer); + if (SDK_INT < 22) { + maybeUpdateBufferPosition(buffer, position, bytesWritten); + } + return bytesWritten; + } catch (android.system.ErrnoException e) { + throw new ErrnoException("write", e.errno); + } + } +} diff --git a/os-compat/src/main/java/androidx/system/OsCompat.java b/os-compat/src/main/java/androidx/system/OsCompat.java new file mode 100644 index 0000000000..4df5c736c2 --- /dev/null +++ b/os-compat/src/main/java/androidx/system/OsCompat.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2011 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.system; + +import java.io.FileDescriptor; +import java.io.InterruptedIOException; +import java.nio.ByteBuffer; + +import static android.os.Build.VERSION.SDK_INT; + +/** + * Access to low-level system functionality. Most of these are system calls. Most users will want + * to use higher-level APIs where available, but this class provides access to the underlying + * primitives used to implement the higher-level APIs. + * + *

The corresponding constants can be found in {@link OsConstantsCompat}. + */ +public final class OsCompat { + private OsCompat() { + } + + private static final Os IMPL; + + static { + if (SDK_INT >= 21) { + IMPL = new OsApi21(); + } else { + IMPL = new OsLibcore(); + } + } + + /** + * See strerror(2). + */ + public static String strerror(int errno) { + return IMPL.strerror(errno); + } + + /** + * See write(2). + */ + public static int write(FileDescriptor fd, ByteBuffer buffer) throws ErrnoException, InterruptedIOException { + return IMPL.write(fd, buffer); + } +} diff --git a/os-compat/src/main/java/androidx/system/OsConstants.java b/os-compat/src/main/java/androidx/system/OsConstants.java new file mode 100644 index 0000000000..efcd3c3c18 --- /dev/null +++ b/os-compat/src/main/java/androidx/system/OsConstants.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2011 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.system; + +interface OsConstants { + + String errnoName(int errno); +} diff --git a/os-compat/src/main/java/androidx/system/OsConstantsApi21.java b/os-compat/src/main/java/androidx/system/OsConstantsApi21.java new file mode 100644 index 0000000000..2108c749df --- /dev/null +++ b/os-compat/src/main/java/androidx/system/OsConstantsApi21.java @@ -0,0 +1,11 @@ +package androidx.system; + +import androidx.annotation.RequiresApi; + +@RequiresApi(21) +final class OsConstantsApi21 implements OsConstants { + @Override + public String errnoName(int errno) { + return android.system.OsConstants.errnoName(errno); + } +} diff --git a/os-compat/src/main/java/androidx/system/OsConstantsCompat.java b/os-compat/src/main/java/androidx/system/OsConstantsCompat.java new file mode 100644 index 0000000000..e753ea5d65 --- /dev/null +++ b/os-compat/src/main/java/androidx/system/OsConstantsCompat.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2011 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.system; + +import android.system.Os; + +import static android.os.Build.VERSION.SDK_INT; + +public final class OsConstantsCompat { + private OsConstantsCompat() { + } + + private static final OsConstants IMPL; + + static { + if (SDK_INT >= 21) { + IMPL = new OsConstantsApi21(); + } else { + IMPL = new OsConstantsLibcore(); + } + } + + /** + * Returns the string name of an errno value. + * For example, "EACCES". See {@link Os#strerror} for human-readable errno descriptions. + */ + public static String errnoName(int errno) { + return IMPL.errnoName(errno); + } +} diff --git a/os-compat/src/main/java/androidx/system/OsConstantsLibcore.java b/os-compat/src/main/java/androidx/system/OsConstantsLibcore.java new file mode 100644 index 0000000000..2473d7e539 --- /dev/null +++ b/os-compat/src/main/java/androidx/system/OsConstantsLibcore.java @@ -0,0 +1,8 @@ +package androidx.system; + +final class OsConstantsLibcore implements OsConstants { + @Override + public String errnoName(int errno) { + return libcore.io.OsConstants.errnoName(errno); + } +} diff --git a/os-compat/src/main/java/androidx/system/OsLibcore.java b/os-compat/src/main/java/androidx/system/OsLibcore.java new file mode 100644 index 0000000000..679f9d2aef --- /dev/null +++ b/os-compat/src/main/java/androidx/system/OsLibcore.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2011 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.system; + +import libcore.io.Libcore; +import libcore.io.OsConstants; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.nio.ByteBuffer; + +import static androidx.system.Os.maybeUpdateBufferPosition; + +final class OsLibcore implements Os { + @Override + public String strerror(int errno) { + return Libcore.os.strerror(errno); + } + + @Override + public int write(FileDescriptor fd, ByteBuffer buffer) throws ErrnoException, InterruptedIOException { + try { + return ioFailureRetry("write", fd, () -> { + final int position = buffer.position(); + final int bytesWritten = Libcore.os.write(fd, buffer); + maybeUpdateBufferPosition(buffer, position, bytesWritten); + return bytesWritten; + }); + } catch (libcore.io.ErrnoException e) { + throw new ErrnoException("write", e.errno); + } + } + + // https://android.googlesource.com/platform/libcore/+/lollipop-release/luni/src/main/native/libcore_io_Posix.cpp#128 + // https://android.googlesource.com/platform/libcore/+/kitkat-release/luni/src/main/java/libcore/io/IoBridge.java#186 + private static int ioFailureRetry(String functionName, FileDescriptor fd, SysCall syscall) + throws libcore.io.ErrnoException, InterruptedIOException { + if (!fd.valid()) { + throw UndeclaredExceptions.raise(new IOException("File descriptor closed")); + } + int rc = -1; + do { + int syscallErrno = 0; + try { + rc = syscall.call(); + } catch (libcore.io.ErrnoException e) { + syscallErrno = e.errno; + } + if (rc == -1 && !fd.valid()) { + throw new InterruptedIOException(functionName + " interrupted"); + } + if (rc == -1 && syscallErrno != OsConstants.EINTR) { + throw new libcore.io.ErrnoException(functionName, syscallErrno); + } + } while (rc == -1); + return rc; + } + + @FunctionalInterface + private interface SysCall { + + Integer call() throws libcore.io.ErrnoException; + } + + // https://dzone.com/articles/throwing-undeclared-checked + private static final class UndeclaredExceptions extends RuntimeException { + private static Throwable sThrowable = null; + + public static synchronized RuntimeException raise(Throwable throwable) { + if (throwable instanceof ReflectiveOperationException || throwable instanceof RuntimeException) { + throw new IllegalArgumentException("Unsupported exception: " + throwable.getClass()); + } + + sThrowable = throwable; + try { + return UndeclaredExceptions.class.newInstance(); + } catch (ReflectiveOperationException e) { + return new RuntimeException(e); + } finally { + sThrowable = null; + } + } + + private UndeclaredExceptions() throws Throwable { + throw sThrowable; + } + } +} diff --git a/server/build.gradle b/server/build.gradle index dbc8261f68..58758438bf 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -4,7 +4,7 @@ android { compileSdkVersion 31 defaultConfig { applicationId "com.genymobile.scrcpy" - minSdkVersion 21 + minSdkVersion 19 targetSdkVersion 31 versionCode 12400 versionName "1.24" @@ -21,6 +21,8 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) testImplementation 'junit:junit:4.13.1' + compileOnly rootProject.fileTree("thirdparty/androidx/annotation/1.3.0/annotation-1.3.0.jar") + implementation project(':os-compat') } apply from: "$project.rootDir/config/android-checkstyle.gradle" diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index c881e38a8b..f67391a780 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -20,12 +20,14 @@ BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0} BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" CLASSES_DIR="$BUILD_DIR/classes" SERVER_DIR=$(dirname "$0") +ROOT_PROJECT_DIR=$(realpath $SERVER_DIR/..) SERVER_BINARY=scrcpy-server ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" echo "Platform: android-$PLATFORM" echo "Build-tools: $BUILD_TOOLS" echo "Build dir: $BUILD_DIR" +echo "Root project dir: $ROOT_PROJECT_DIR" rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy" @@ -48,8 +50,15 @@ cd "$SERVER_DIR/src/main/aidl" echo "Compiling java sources..." cd ../java -javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \ +classpath="$CLASSES_DIR" +classpath="$classpath:$ROOT_PROJECT_DIR/thirdparty/androidx/annotation/1.3.0/annotation-1.3.0.jar" +# https://stackoverflow.com/a/58768648/2444099 +classpath="$classpath:$ANDROID_HOME/build-tools/$BUILD_TOOLS/core-lambda-stubs.jar" +javac -bootclasspath "$ANDROID_JAR" -cp "$classpath" -d "$CLASSES_DIR" \ + -encoding UTF-8 \ -source 1.8 -target 1.8 \ + $ROOT_PROJECT_DIR/os-compat/src/main/java/androidx/system/*.java \ + $ROOT_PROJECT_DIR/libcore/src/main/java/libcore/io/*.java \ com/genymobile/scrcpy/*.java \ com/genymobile/scrcpy/wrappers/*.java @@ -61,6 +70,7 @@ then # use dx "$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \ --output "$BUILD_DIR/classes.dex" \ + androidx/system/*.class \ android/view/*.class \ android/content/*.class \ com/genymobile/scrcpy/*.class \ @@ -74,6 +84,7 @@ else # use d8 "$ANDROID_HOME/build-tools/$BUILD_TOOLS/d8" --classpath "$ANDROID_JAR" \ --output "$BUILD_DIR/classes.zip" \ + androidx/system/*.class \ android/view/*.class \ android/content/*.class \ com/genymobile/scrcpy/*.class \ diff --git a/server/src/main/java/com/genymobile/scrcpy/IO.java b/server/src/main/java/com/genymobile/scrcpy/IO.java index 57c017dbee..b3b6c247e4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/IO.java @@ -1,8 +1,7 @@ package com.genymobile.scrcpy; -import android.system.ErrnoException; -import android.system.Os; -import android.system.OsConstants; +import androidx.system.ErrnoException; +import androidx.system.OsCompat; import java.io.FileDescriptor; import java.io.IOException; @@ -14,23 +13,12 @@ private IO() { } public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { - // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so - // count the remaining bytes manually. - // See . - int remaining = from.remaining(); - while (remaining > 0) { - try { - int w = Os.write(fd, from); - if (BuildConfig.DEBUG && w < 0) { - // w should not be negative, since an exception is thrown on error - throw new AssertionError("Os.write() returned a negative value (" + w + ")"); - } - remaining -= w; - } catch (ErrnoException e) { - if (e.errno != OsConstants.EINTR) { - throw new IOException(e); - } + try { + while (from.hasRemaining()) { + OsCompat.write(fd, from); } + } catch (ErrnoException e) { + throw new IOException(e); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/MediaCodecCompat.java b/server/src/main/java/com/genymobile/scrcpy/MediaCodecCompat.java new file mode 100644 index 0000000000..5e2741c9a9 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/MediaCodecCompat.java @@ -0,0 +1,116 @@ +package com.genymobile.scrcpy; + +import android.media.MediaCodec; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.nio.ByteBuffer; + +import static android.media.MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED; +import static android.os.Build.VERSION.SDK_INT; + +/** + * Version of {@link MediaCodec} that backports {@link #getOutputBuffer} to Kitkat. + * The backported implementation isn't thread safe. + */ +abstract class MediaCodecCompat { + private final MediaCodec delegate; + + MediaCodecCompat(@NonNull MediaCodec delegate) { + this.delegate = delegate; + } + + @NonNull + protected MediaCodec getDelegate() { + return delegate; + } + + @NonNull + static MediaCodecCompat wrap(@NonNull MediaCodec codec) { + if (SDK_INT >= 21) { + return new Platform(codec); + } else { + return new Backport(codec); + } + } + + abstract int dequeueOutputBuffer( + @NonNull MediaCodec.BufferInfo info, long timeoutUs); + + @Nullable + abstract ByteBuffer getOutputBuffer(int index); + + abstract void releaseOutputBuffer(int index, boolean render); + + @SuppressWarnings("deprecation") + private static class Backport extends MediaCodecCompat { + private ByteBuffer[] cachedOutputBuffers = null; + + Backport(@NonNull MediaCodec delegate) { + super(delegate); + } + + @Override + int dequeueOutputBuffer( + @NonNull MediaCodec.BufferInfo info, long timeoutUs) { + final int res = getDelegate().dequeueOutputBuffer(info, timeoutUs); + if (res == INFO_OUTPUT_BUFFERS_CHANGED) { + cachedOutputBuffers = null; + } + return res; + } + + @Nullable + @Override + ByteBuffer getOutputBuffer(int index) { + if (cachedOutputBuffers == null) { + cacheOutputBuffers(); + } + if (cachedOutputBuffers == null) { + throw new IllegalStateException(); + } + return cachedOutputBuffers[index]; + } + + @Override + void releaseOutputBuffer(int index, boolean render) { + cachedOutputBuffers = null; + getDelegate().releaseOutputBuffer(index, render); + } + + private void cacheOutputBuffers() { + ByteBuffer[] buffers = null; + try { + buffers = getDelegate().getOutputBuffers(); + } catch (IllegalStateException e) { + // we don't get buffers in async mode + } + cachedOutputBuffers = buffers; + } + } + + @RequiresApi(21) + private static class Platform extends MediaCodecCompat { + Platform(@NonNull MediaCodec delegate) { + super(delegate); + } + + @Override + int dequeueOutputBuffer( + @NonNull MediaCodec.BufferInfo info, long timeoutUs) { + return getDelegate().dequeueOutputBuffer(info, timeoutUs); + } + + @Nullable + @Override + ByteBuffer getOutputBuffer(int index) { + return getDelegate().getOutputBuffer(index); + } + + @Override + void releaseOutputBuffer(int index, boolean render) { + getDelegate().releaseOutputBuffer(index, render); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/MediaCodecListCompat.java b/server/src/main/java/com/genymobile/scrcpy/MediaCodecListCompat.java new file mode 100644 index 0000000000..07ecfd8f08 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/MediaCodecListCompat.java @@ -0,0 +1,87 @@ +package com.genymobile.scrcpy; + +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static android.media.MediaCodecList.REGULAR_CODECS; +import static android.os.Build.VERSION.SDK_INT; + +/** + * Version of {@link MediaCodecList} that backports {@link #getCodecInfos()} to Kitkat. + */ +abstract class MediaCodecListCompat { + MediaCodecListCompat() { + } + + /** + * Returns the list of codecs that are suitable + * for regular (buffer-to-buffer) decoding or encoding. + */ + static MediaCodecListCompat regular() { + if (SDK_INT >= 21) { + return new Platform(); + } else { + return new Backport(); + } + } + + /** + * Returns the list of {@link MediaCodecInfo} objects for the list + * of media-codecs. + */ + @NonNull + abstract MediaCodecInfo[] getCodecInfos(); + + @NonNull + final MediaCodecInfo[] getEncoderInfosForType(@NonNull String mimeType) { + List result = new ArrayList<>(); + MediaCodecInfo[] codecInfos = MediaCodecListCompat.regular().getCodecInfos(); + for (MediaCodecInfo codecInfo : codecInfos) { + if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) { + result.add(codecInfo); + } + } + return result.toArray(new MediaCodecInfo[result.size()]); + } + + @SuppressWarnings("deprecation") + private static class Backport extends MediaCodecListCompat { + private final MediaCodecInfo[] mCodecInfos; + + Backport() { + final int size = MediaCodecList.getCodecCount(); + final MediaCodecInfo[] codecInfos = new MediaCodecInfo[size]; + for (int i = 0; i < size; i++) { + codecInfos[i] = MediaCodecList.getCodecInfoAt(i); + } + this.mCodecInfos = codecInfos; + } + + @NonNull + @Override + public final MediaCodecInfo[] getCodecInfos() { + return Arrays.copyOf(mCodecInfos, mCodecInfos.length); + } + } + + @RequiresApi(21) + private static class Platform extends MediaCodecListCompat { + private final MediaCodecList mDelegate; + + Platform() { + this.mDelegate = new MediaCodecList(REGULAR_CODECS); + } + + @NonNull + @Override + MediaCodecInfo[] getCodecInfos() { + return mDelegate.getCodecInfos(); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index e95896d370..2928af5053 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -2,10 +2,10 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl; +import android.annotation.SuppressLint; import android.graphics.Rect; import android.media.MediaCodec; import android.media.MediaCodecInfo; -import android.media.MediaCodecList; import android.media.MediaFormat; import android.os.Build; import android.os.IBinder; @@ -14,8 +14,6 @@ import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -25,6 +23,9 @@ public class ScreenEncoder implements Device.RotationListener { private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder"; + @SuppressLint("InlinedApi") + public static final String MIMETYPE_VIDEO_AVC = MediaFormat.MIMETYPE_VIDEO_AVC; + // Keep the values in descending order private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; @@ -144,7 +145,8 @@ private static int chooseMaxSizeFallback(Size failedSize) { return 0; } - private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException { + private boolean encode(MediaCodec platformCodec, FileDescriptor fd) throws IOException { + final MediaCodecCompat codec = MediaCodecCompat.wrap(platformCodec); boolean eof = false; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); @@ -201,28 +203,18 @@ private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, IO.writeFully(fd, headerBuffer); } - private static MediaCodecInfo[] listEncoders() { - List result = new ArrayList<>(); - MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); - for (MediaCodecInfo codecInfo : list.getCodecInfos()) { - if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) { - result.add(codecInfo); - } - } - return result.toArray(new MediaCodecInfo[result.size()]); - } - private static MediaCodec createCodec(String encoderName) throws IOException { if (encoderName != null) { Ln.d("Creating encoder by name: '" + encoderName + "'"); try { return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { - MediaCodecInfo[] encoders = listEncoders(); + MediaCodecInfo[] encoders = MediaCodecListCompat.regular() + .getEncoderInfosForType(MIMETYPE_VIDEO_AVC); throw new InvalidEncoderException(encoderName, encoders); } } - MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); + MediaCodec codec = MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC); Ln.d("Using encoder: '" + codec.getName() + "'"); return codec; } @@ -246,7 +238,7 @@ private static void setCodecOption(MediaFormat format, CodecOption codecOption) private static MediaFormat createFormat(int bitRate, int maxFps, List codecOptions) { MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); + format.setString(MediaFormat.KEY_MIME, MIMETYPE_VIDEO_AVC); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); // must be present to configure the encoder, but does not impact the actual frame rate, which is variable format.setInteger(MediaFormat.KEY_FRAME_RATE, 60); diff --git a/settings.gradle b/settings.gradle index 8f51035f9b..c1781cf29e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,3 @@ include ':server' +include ':os-compat' +include ':libcore' diff --git a/thirdparty/androidx/annotation/1.3.0/annotation-1.3.0.jar b/thirdparty/androidx/annotation/1.3.0/annotation-1.3.0.jar new file mode 100644 index 0000000000..10c7c9d0c0 Binary files /dev/null and b/thirdparty/androidx/annotation/1.3.0/annotation-1.3.0.jar differ