mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 11:31:08 +00:00
feat: target Android API 36
This commit is contained in:
1 parent
e5ba165027
commit
0fd50d76b2
11 files changed
+103
-35
No files matched your search
+2
-13
@@ -80,7 +80,7 @@ def variantSuffixes = rootProject.file('tools/release/variants.txt').readLines()
|
||||
|
||||
android {
|
||||
namespace 'com.droiddeck.launcher'
|
||||
compileSdk 34
|
||||
compileSdk 36
|
||||
// The NDK is pinned by property so CI can hand in whatever the runner already has rather than
|
||||
// downloading a second copy; see gradle.properties.
|
||||
ndkVersion project.findProperty('ndkVersion') ?: '27.3.13750724'
|
||||
@@ -88,10 +88,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId "com.droiddeck.launcher"
|
||||
minSdkVersion 26
|
||||
// 28 is not a leftover. An app on targetSdk 29+ runs in a domain that may not execute a
|
||||
// file out of its own data directory, and this app's entire job is to exec a rootfs:
|
||||
// proot, gamescope, the Steam client and everything it launches all live under files/.
|
||||
targetSdkVersion 28
|
||||
targetSdkVersion 36
|
||||
versionCode 12
|
||||
versionName "0.3.2"
|
||||
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
|
||||
@@ -152,14 +149,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
// targetSdk 28 is deliberate and structural: an app on 29+ may not execute a file out of
|
||||
// its own data directory, and executing a rootfs is the whole of what this app does. Lint
|
||||
// fails a release build over it because Google Play will not accept an sdk that old -
|
||||
// true, and not something this app can trade away. Every other check stays on.
|
||||
disable 'ExpiredTargetSdkVersion'
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
// Kadb's Bouncy Castle artifacts repeat the same multi-release JAR manifest.
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
process at perceptible priority, and a high-performance WiFi lock keeps a backgrounded
|
||||
download from being throttled to nothing by the radio's power-save. -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<!-- Optional Wi-Fi discovery: requested only from Steam settings after an explanation. -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
@@ -30,9 +32,9 @@
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<!-- Installs DroidDeck's own updates (Updates page) over itself, after the user allows it. -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<!-- A paired Steam Controller, read over Bluetooth LE out of lizard mode (SteamControllerBle).
|
||||
Granted at install below targetSdk 31; no scan, so no location. -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<!-- A paired Steam Controller, read over Bluetooth LE out of lizard mode (SteamControllerBle). -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||
@@ -47,6 +49,7 @@
|
||||
android:hardwareAccelerated="true"
|
||||
android:largeHeap="true"
|
||||
android:extractNativeLibs="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:appCategory="game"
|
||||
android:isGame="true">
|
||||
|
||||
@@ -167,14 +170,25 @@
|
||||
<service
|
||||
android:name=".session.SessionService"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false" />
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:stopWithTask="false">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Runs the user's Linux game session (compositor, Steam client, audio) while the app is not on screen" />
|
||||
</service>
|
||||
<!-- Holds the process in the foreground while the Linux runtime downloads and unpacks. -->
|
||||
<service
|
||||
android:name=".runtime.RuntimeInstallService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name=".core.WirelessAdbPairingService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Waits for the wireless debugging pairing code the user enters from the notification" />
|
||||
</service>
|
||||
<!-- Keeps the process alive while a store (GOG, Epic, Amazon) game downloads. -->
|
||||
<service
|
||||
android:name=".stores.download.StoreDownloadService"
|
||||
|
||||
@@ -893,12 +893,17 @@ class MainActivity : ComponentActivity() {
|
||||
if (savedInstanceState == null) ui.post { startSteamAtStartupIfEnabled() }
|
||||
|
||||
// The session's logs land in Downloads so a failed run can be handed over as a folder
|
||||
// rather than dug out of app-private storage. targetSdk 28 means the old permission still
|
||||
// grants exactly that.
|
||||
// rather than dug out of app-private storage.
|
||||
val wanted = ArrayList<String>()
|
||||
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
|
||||
&& checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
wanted.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !SessionPrefs.bluetoothAsked(this)
|
||||
&& checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
|
||||
wanted.add(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
SessionPrefs.setBluetoothAsked(this)
|
||||
}
|
||||
// The microphone is off by default. Someone who turned it on before the permission was
|
||||
// granted is asked once here, with the storage prompt; a refusal is not asked again - the
|
||||
// toggle asks when used.
|
||||
|
||||
@@ -156,7 +156,7 @@ public class PulseAudioComponent extends SessionPart {
|
||||
env.add("HOME=" + workingDir);
|
||||
env.add("TMPDIR=" + workingDir);
|
||||
|
||||
String command = workingDir.getAbsolutePath() + "/libpulseaudio.so"
|
||||
String command = HostProcess.LINKER + " " + workingDir.getAbsolutePath() + "/libpulseaudio.so"
|
||||
+ " --system=false --disable-shm=true --fail=false"
|
||||
+ " -n --file=default.pa --daemonize=false --use-pid-file=false --exit-idle-time=-1"
|
||||
// Info level: the classic sink reports the stream Android granted (burst, capacity,
|
||||
@@ -216,6 +216,7 @@ public class PulseAudioComponent extends SessionPart {
|
||||
try {
|
||||
File modules = new File(workingDir, "modules/arm64");
|
||||
ArrayList<String> cmd = new ArrayList<>();
|
||||
cmd.add(HostProcess.LINKER);
|
||||
cmd.add(pactl.getAbsolutePath());
|
||||
java.util.Collections.addAll(cmd, args);
|
||||
ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||
@@ -269,7 +270,7 @@ public class PulseAudioComponent extends SessionPart {
|
||||
try {
|
||||
File modules = new File(workingDir, "modules/arm64");
|
||||
ProcessBuilder builder = new ProcessBuilder(
|
||||
pactl.getAbsolutePath(), "suspend-sink", "@DEFAULT_SINK@", Boolean.toString(suspended));
|
||||
HostProcess.LINKER, pactl.getAbsolutePath(), "suspend-sink", "@DEFAULT_SINK@", Boolean.toString(suspended));
|
||||
builder.directory(workingDir);
|
||||
builder.redirectErrorStream(true);
|
||||
builder.redirectOutput(new File("/dev/null"));
|
||||
|
||||
@@ -14,6 +14,8 @@ import java.util.function.Consumer
|
||||
* block the child the moment the pipe filled.
|
||||
*/
|
||||
object HostProcess {
|
||||
const val LINKER = "/system/bin/linker64"
|
||||
|
||||
private const val TAG = "HostProcess"
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,10 +11,10 @@ import com.droiddeck.launcher.session.SessionPrefs
|
||||
object WifiDiscovery {
|
||||
val permissions = arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
|
||||
// targetSdk 28 accepts either grant for Wi-Fi scanning.
|
||||
fun permissionGranted(context: Context): Boolean = permissions.any {
|
||||
context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
fun permissionGranted(context: Context): Boolean =
|
||||
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) else permissions).any {
|
||||
context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
fun locationEnabled(context: Context): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
|
||||
(context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager)?.isLocationEnabled == true
|
||||
|
||||
@@ -36,7 +36,6 @@ class FilePickerActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
requestedOrientation = com.droiddeck.launcher.session.SessionPrefs.orientation(this)
|
||||
// targetSdk 28: the old storage permission is exactly what lets us list the card.
|
||||
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 1)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import java.util.UUID
|
||||
*
|
||||
* Lizard mode is refreshed while connected; when a session ends, the controller restores it itself.
|
||||
*/
|
||||
@SuppressLint("MissingPermission") // BLUETOOTH is granted at install below targetSdk 31
|
||||
@SuppressLint("MissingPermission")
|
||||
class SteamControllerBle(private val context: Context) {
|
||||
private val thread = HandlerThread("steam-controller-ble").apply { start() }
|
||||
private val handler = Handler(thread.looper)
|
||||
|
||||
@@ -20,8 +20,7 @@ import java.util.List;
|
||||
* The glibc arm64 rootfs at {@code files/linuxfs} and the proot invocation that runs a program in
|
||||
* it as this app's own uid. It is a second runtime beside the Wine imagefs, not a container: no
|
||||
* Wine, no box64, no FEX. proot is packaged as {@code libproot.so} so the installer places it, with
|
||||
* its loader, in the native library directory - the only place an app on targetSdk 28 may execute
|
||||
* a file from.
|
||||
* its loader, in the native library directory.
|
||||
*
|
||||
* <p>Ported from WinNative's gamescope runtime (GPL-3.0).
|
||||
*/
|
||||
|
||||
@@ -259,6 +259,12 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("micAsked", true).apply()
|
||||
}
|
||||
|
||||
fun bluetoothAsked(context: Context): Boolean = prefs(context).getBoolean("bluetoothAsked", false)
|
||||
|
||||
fun setBluetoothAsked(context: Context) {
|
||||
prefs(context).edit().putBoolean("bluetoothAsked", true).apply()
|
||||
}
|
||||
|
||||
fun setMicEnabled(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("mic", on).apply()
|
||||
}
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
* noexec.c: FEX runs an x86 program and its libraries where they lie, on noexec shared storage too.
|
||||
* FEX maps them with the guest's PROT_EXEC, which the kernel refuses for a file on a noexec mount,
|
||||
* but it never runs those pages itself - it runs its own translation of them - so a mapping or an
|
||||
* mprotect refused for that alone is made without PROT_EXEC. Only in FEX's own processes.
|
||||
* mprotect refused for that alone is made without PROT_EXEC.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* NOEXEC_TEST: tools/tests builds it for the host. */
|
||||
@@ -45,11 +49,60 @@ __attribute__((constructor)) static void noexec_init(void) {
|
||||
real_mprotect = (int (*)(void *, size_t, int))dlsym(RTLD_NEXT, "mprotect");
|
||||
}
|
||||
|
||||
static int private_file_mapping(uintptr_t start, uintptr_t end) {
|
||||
int fd = (int)syscall(SYS_openat, AT_FDCWD, "/proc/self/maps", O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) return 0;
|
||||
char buf[4096], line[1024];
|
||||
size_t used = 0;
|
||||
int found = 0, done = 0;
|
||||
ssize_t n;
|
||||
while (!done && (n = syscall(SYS_read, fd, buf, sizeof buf)) > 0) {
|
||||
for (ssize_t i = 0; i < n && !done; i++) {
|
||||
if (buf[i] != '\n') {
|
||||
if (used < sizeof line - 1) line[used++] = buf[i];
|
||||
continue;
|
||||
}
|
||||
line[used] = 0;
|
||||
used = 0;
|
||||
char *p = line;
|
||||
uintptr_t lo = strtoull(p, &p, 16), hi = strtoull(p + 1, &p, 16);
|
||||
if (start < lo || start >= hi) continue;
|
||||
done = 1;
|
||||
char *perms = p + 1;
|
||||
if (end > hi || perms[3] != 'p') break;
|
||||
char *path = strchr(perms, '/');
|
||||
struct statvfs vfs;
|
||||
found = path && !(statvfs(path, &vfs) == 0 && (vfs.f_flag & ST_NOEXEC));
|
||||
}
|
||||
}
|
||||
syscall(SYS_close, fd);
|
||||
return found;
|
||||
}
|
||||
|
||||
static int rebake(void *addr, size_t len, int prot) {
|
||||
if (real_mprotect(addr, len, PROT_READ)) return -1;
|
||||
void *copy = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (copy == MAP_FAILED) return -1;
|
||||
memcpy(copy, addr, len);
|
||||
if (mmap(addr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == MAP_FAILED) {
|
||||
munmap(copy, len);
|
||||
return -1;
|
||||
}
|
||||
memcpy(addr, copy, len);
|
||||
munmap(copy, len);
|
||||
__builtin___clear_cache((char *)addr, (char *)addr + len);
|
||||
return real_mprotect(addr, len, prot);
|
||||
}
|
||||
|
||||
int mprotect(void *addr, size_t len, int prot) {
|
||||
if (!real_mprotect) noexec_init();
|
||||
int result = real_mprotect(addr, len, prot);
|
||||
if (result && errno == EACCES && (prot & PROT_EXEC) && in_fex()) {
|
||||
result = real_mprotect(addr, len, prot & ~PROT_EXEC);
|
||||
if (result && errno == EACCES && (prot & PROT_EXEC)) {
|
||||
if (in_fex()) {
|
||||
result = real_mprotect(addr, len, prot & ~PROT_EXEC);
|
||||
} else if (len && private_file_mapping((uintptr_t)addr, (uintptr_t)addr + len)) {
|
||||
result = rebake(addr, len, prot);
|
||||
}
|
||||
if (result) errno = EACCES;
|
||||
}
|
||||
return result;
|
||||
|
||||
Reference in new issue
Block a user