From a82a1d837e5112d7933379796f17072e0fc8f266 Mon Sep 17 00:00:00 2001 From: Eugen Pechanec Date: Mon, 27 Jun 2022 03:45:09 +0200 Subject: [PATCH] Replace shadowed android.system with custom androidx.system package So that we don't shadow framework classes in our binary. Update buffer position in OsCompat.write (fixed in Lollipop MR1). Synthesize InterruptedIOException in Kitkat like in Lollipop. OsCompat never throws EINTR errno. --- config/checkstyle/checkstyle.xml | 6 -- libcore/src/main/java/libcore/io/Os.java | 5 +- os-compat/build.gradle | 24 +++++ .../java/androidx}/system/ErrnoException.java | 34 +++--- .../src/main/java/androidx/system/Os.java | 33 ++++++ .../main/java/androidx/system/OsApi21.java | 47 ++++++++ .../main/java/androidx/system/OsCompat.java | 53 ++++----- .../java/androidx/system/OsConstants.java | 21 ++++ .../androidx/system/OsConstantsApi21.java | 11 ++ .../androidx/system/OsConstantsCompat.java | 24 +++-- .../androidx/system/OsConstantsLibcore.java | 8 ++ .../main/java/androidx/system/OsLibcore.java | 102 ++++++++++++++++++ server/build.gradle | 2 +- server/build_without_gradle.sh | 8 +- .../main/java/com/genymobile/scrcpy/IO.java | 28 ++--- settings.gradle | 1 + 16 files changed, 321 insertions(+), 86 deletions(-) create mode 100644 os-compat/build.gradle rename {server/src/main/java/android => os-compat/src/main/java/androidx}/system/ErrnoException.java (83%) create mode 100644 os-compat/src/main/java/androidx/system/Os.java create mode 100644 os-compat/src/main/java/androidx/system/OsApi21.java rename server/src/main/java/android/system/Os.java => os-compat/src/main/java/androidx/system/OsCompat.java (57%) create mode 100644 os-compat/src/main/java/androidx/system/OsConstants.java create mode 100644 os-compat/src/main/java/androidx/system/OsConstantsApi21.java rename server/src/main/java/android/system/OsConstants.java => os-compat/src/main/java/androidx/system/OsConstantsCompat.java (66%) create mode 100644 os-compat/src/main/java/androidx/system/OsConstantsLibcore.java create mode 100644 os-compat/src/main/java/androidx/system/OsLibcore.java diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 77d1f56f..ca0d813b 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -154,10 +154,4 @@ page at http://checkstyle.sourceforge.net/config.html --> - - - - - - diff --git a/libcore/src/main/java/libcore/io/Os.java b/libcore/src/main/java/libcore/io/Os.java index 4a9e61a8..74f08ff9 100644 --- a/libcore/src/main/java/libcore/io/Os.java +++ b/libcore/src/main/java/libcore/io/Os.java @@ -19,7 +19,6 @@ import java.io.FileDescriptor; import java.nio.ByteBuffer; public interface Os { - public String strerror(int errno); - - public int write(FileDescriptor fd, ByteBuffer buffer) throws ErrnoException; + String strerror(int errno); + int write(FileDescriptor fd, ByteBuffer buffer) throws ErrnoException; } diff --git a/os-compat/build.gradle b/os-compat/build.gradle new file mode 100644 index 00000000..e85d6cfb --- /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/server/src/main/java/android/system/ErrnoException.java b/os-compat/src/main/java/androidx/system/ErrnoException.java similarity index 83% rename from server/src/main/java/android/system/ErrnoException.java rename to os-compat/src/main/java/androidx/system/ErrnoException.java index 75d90193..b39d159b 100644 --- a/server/src/main/java/android/system/ErrnoException.java +++ b/os-compat/src/main/java/androidx/system/ErrnoException.java @@ -13,30 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package android.system; - -import android.annotation.TargetApi; +package androidx.system; import java.io.IOException; import java.net.SocketException; -import libcore.io.Libcore; - /** - * 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 + * 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. */ -@SuppressWarnings("unused") -@TargetApi(21) public final class ErrnoException extends Exception { private final String functionName; - - /** - * The errno value, for comparison with the {@code E} constants in {@link OsConstants}. - */ - public final int errno; + private final int errno; /** * Constructs an instance with the given function name and errno value. @@ -55,17 +44,24 @@ public final class ErrnoException extends Exception { 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 = OsConstants.errnoName(errno); + String errnoName = OsConstantsCompat.errnoName(errno); if (errnoName == null) { errnoName = "errno " + errno; } - String description = Libcore.os.strerror(errno); + String description = OsCompat.strerror(errno); return functionName + " failed: " + errnoName + " (" + description + ")"; } @@ -77,9 +73,7 @@ public final class ErrnoException extends Exception { * {@code throw e.rethrowAsIOException()} to make that clear to the compiler. */ public IOException rethrowAsIOException() throws IOException { - IOException newException = new IOException(getMessage()); - newException.initCause(this); - throw newException; + throw new IOException(getMessage(), this); } /** 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 00000000..32d6b495 --- /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 00000000..973aa4df --- /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/server/src/main/java/android/system/Os.java b/os-compat/src/main/java/androidx/system/OsCompat.java similarity index 57% rename from server/src/main/java/android/system/Os.java rename to os-compat/src/main/java/androidx/system/OsCompat.java index 456179ca..4df5c736 100644 --- a/server/src/main/java/android/system/Os.java +++ b/os-compat/src/main/java/androidx/system/OsCompat.java @@ -13,41 +13,46 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package android.system; - -import android.annotation.TargetApi; +package androidx.system; import java.io.FileDescriptor; import java.io.InterruptedIOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.net.SocketException; import java.nio.ByteBuffer; -import libcore.io.Libcore; +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 OsConstants}. + *

The corresponding constants can be found in {@link OsConstantsCompat}. */ -@SuppressWarnings("unused") -@TargetApi(21) -public final class Os { - private Os() {} - - /** - * See write(2). - */ - public static int write(FileDescriptor fd, ByteBuffer buffer) throws ErrnoException, InterruptedIOException { - try { - return Libcore.os.write(fd, buffer); - } catch (libcore.io.ErrnoException e) { - throw new ErrnoException("write", e.errno); +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 00000000..efcd3c3c --- /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 00000000..2108c749 --- /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/server/src/main/java/android/system/OsConstants.java b/os-compat/src/main/java/androidx/system/OsConstantsCompat.java similarity index 66% rename from server/src/main/java/android/system/OsConstants.java rename to os-compat/src/main/java/androidx/system/OsConstantsCompat.java index 8580c6b6..e753ea5d 100644 --- a/server/src/main/java/android/system/OsConstants.java +++ b/os-compat/src/main/java/androidx/system/OsConstantsCompat.java @@ -13,23 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.system; +package androidx.system; -import android.annotation.TargetApi; +import android.system.Os; -@SuppressWarnings("unused") -@TargetApi(21) -public final class OsConstants { +import static android.os.Build.VERSION.SDK_INT; - private OsConstants() { } +public final class OsConstantsCompat { + private OsConstantsCompat() { + } - public static final int EINTR = libcore.io.OsConstants.EINTR; + 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 libcore.io.OsConstants.errnoName(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 00000000..2473d7e5 --- /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 00000000..679f9d2a --- /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 dd7591e2..58758438 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -22,7 +22,7 @@ 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") - compileOnly project(':libcore') + 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 2908e242..f67391a7 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -52,10 +52,12 @@ echo "Compiling java sources..." cd ../java 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 \ - android/system/*.java \ + $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 @@ -68,7 +70,7 @@ then # use dx "$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \ --output "$BUILD_DIR/classes.dex" \ - android/system/*.class \ + androidx/system/*.class \ android/view/*.class \ android/content/*.class \ com/genymobile/scrcpy/*.class \ @@ -82,7 +84,7 @@ else # use d8 "$ANDROID_HOME/build-tools/$BUILD_TOOLS/d8" --classpath "$ANDROID_JAR" \ --output "$BUILD_DIR/classes.zip" \ - android/system/*.class \ + 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 bc2f37bd..b3b6c247 100644 --- a/server/src/main/java/com/genymobile/scrcpy/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/IO.java @@ -1,9 +1,7 @@ package com.genymobile.scrcpy; -import android.annotation.TargetApi; -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,25 +12,13 @@ public final class IO { // not instantiable } - @TargetApi(21) // Backported subset of android.system. 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/settings.gradle b/settings.gradle index 31c8f101..c1781cf2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,3 @@ include ':server' +include ':os-compat' include ':libcore'