mirror of
https://github.com/Benjamin-Wiegand/Flywheel.git
synced 2026-07-20 05:22:25 -07:00
feat: support launching privd via Shizuku
This commit is contained in:
@@ -18,6 +18,10 @@ android {
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
@@ -41,6 +45,8 @@ dependencies {
|
||||
implementation(libs.bouncycastle)
|
||||
implementation(libs.localbroadcastmanager)
|
||||
implementation(libs.preference)
|
||||
implementation(libs.shizuku.api)
|
||||
implementation(libs.shizuku.provider)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.ext.junit)
|
||||
androidTestImplementation(libs.espresso.core)
|
||||
|
||||
@@ -109,13 +109,22 @@
|
||||
<receiver
|
||||
android:name=".PrivdConnectionReceiver"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedReceiver"
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="io.benwiegand.projection.geargrinder.BIND_PRIVD" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
android:name="rikka.shizuku.ShizukuProvider"
|
||||
android:authorities="${applicationId}.shizuku"
|
||||
android:multiprocess="false"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL"
|
||||
/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.benwiegand.projection.geargrinder;
|
||||
|
||||
interface IShizukuUserService {
|
||||
void execPrivd(String scriptPath, in Map<String, String> env) = 1;
|
||||
void killPrivd() = 2;
|
||||
|
||||
void destroy() = 16777114;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package io.benwiegand.projection.geargrinder;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.hardware.usb.UsbAccessory;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.media.projection.MediaProjectionConfig;
|
||||
@@ -17,10 +19,13 @@ import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import rikka.shizuku.Shizuku;
|
||||
|
||||
public class ConnectionRequestActivity extends AppCompatActivity {
|
||||
private static final String TAG = ConnectionRequestActivity.class.getSimpleName();
|
||||
|
||||
public static final String INTENT_ACTION_REQUEST_MEDIA_PROJECTION = "io.benwiegand.projection.geargrinder.REQUEST_MEDIA_PROJECTION";
|
||||
public static final String INTENT_ACTION_REQUEST_SHIZUKU = "io.benwiegand.projection.geargrinder.REQUEST_SHIZUKU";
|
||||
|
||||
private static final String INTENT_ACTION_USB_PERMISSION_RESULT = "io.benwiegand.projection.geargrinder.USB_PERMISSION";
|
||||
|
||||
@@ -29,9 +34,18 @@ public class ConnectionRequestActivity extends AppCompatActivity {
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(new View(this));
|
||||
|
||||
Shizuku.addRequestPermissionResultListener(this::onShizukuPermissionResult);
|
||||
|
||||
onIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
Shizuku.removeRequestPermissionResultListener(this::onShizukuPermissionResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
@@ -86,6 +100,45 @@ public class ConnectionRequestActivity extends AppCompatActivity {
|
||||
requestMediaProjection();
|
||||
return; // not done yet
|
||||
}
|
||||
case INTENT_ACTION_REQUEST_SHIZUKU -> {
|
||||
if (Shizuku.isPreV11()) {
|
||||
Log.e(TAG, "shizuku app too old");
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.shizuku_permission_request)
|
||||
.setMessage(R.string.shizuku_too_old)
|
||||
.setPositiveButton(R.string.close_button, (d, i) -> finish())
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Shizuku.getBinder() == null) {
|
||||
Log.e(TAG, "shizuku binder is null. is shizuku running?");
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.shizuku_permission_request)
|
||||
.setMessage(R.string.shizuku_not_running)
|
||||
.setPositiveButton(R.string.close_button, (d, i) -> finish())
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i(TAG, "shizuku already granted: uid = " + Shizuku.getUid());
|
||||
finish();
|
||||
} else if (Shizuku.shouldShowRequestPermissionRationale()) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.shizuku_permission_request)
|
||||
.setMessage(R.string.shizuku_permission_rationale)
|
||||
.setPositiveButton(R.string.grant_permission_button, (d, i) ->
|
||||
Shizuku.requestPermission(69))
|
||||
.setNegativeButton(R.string.not_now_button, (d, i) -> finish())
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
} else {
|
||||
Shizuku.requestPermission(69);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finish();
|
||||
@@ -107,6 +160,19 @@ public class ConnectionRequestActivity extends AppCompatActivity {
|
||||
}
|
||||
);
|
||||
|
||||
private void onShizukuPermissionResult(int requestCode, int result) {
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.e(TAG, "shizuku permission denied");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "shizuku granted: uid = " + Shizuku.getUid());
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
private void connectToUsbHeadunit() {
|
||||
Log.i(TAG, "starting connection service");
|
||||
startService(new Intent(this, ConnectionService.class)
|
||||
|
||||
@@ -15,7 +15,8 @@ import android.util.Log;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.privileged.RootPrivdLauncher;
|
||||
import io.benwiegand.projection.geargrinder.settings.SettingsManager;
|
||||
import io.benwiegand.projection.geargrinder.privileged.PrivdLauncher;
|
||||
|
||||
public class PrivdConnectionReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = PrivdConnectionReceiver.class.getSimpleName();
|
||||
@@ -31,8 +32,12 @@ public class PrivdConnectionReceiver extends BroadcastReceiver {
|
||||
if (suppliedToken == null) return;
|
||||
if (suppliedToken.length != TOKEN_LENGTH) return;
|
||||
|
||||
SettingsManager settings = new SettingsManager(context);
|
||||
PrivdLauncher launcher = PrivdLauncher.createForPrivilegeMode(settings.getPrivilegeMode(), context);
|
||||
if (launcher == null) return;
|
||||
|
||||
try {
|
||||
byte[] token = RootPrivdLauncher.readToken(context);
|
||||
byte[] token = launcher.readToken();
|
||||
if (token == null) return;
|
||||
if (!Arrays.equals(token, suppliedToken)) return;
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -22,7 +22,10 @@ import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.callback.IPCConnectionListener;
|
||||
import io.benwiegand.projection.geargrinder.privileged.RootPrivdLauncher;
|
||||
import io.benwiegand.projection.geargrinder.settings.PrivilegeMode;
|
||||
import io.benwiegand.projection.geargrinder.settings.SettingsManager;
|
||||
import io.benwiegand.projection.geargrinder.privileged.PrivdLauncher;
|
||||
import io.benwiegand.projection.geargrinder.privileged.ShizukuPrivdLauncher;
|
||||
import io.benwiegand.projection.libprivd.IPrivd;
|
||||
|
||||
public class PrivdService extends Service {
|
||||
@@ -35,7 +38,7 @@ public class PrivdService extends Service {
|
||||
|
||||
private final List<IPCConnectionListener> ipcConnectionListeners = new LinkedList<>();
|
||||
|
||||
private RootPrivdLauncher privdLauncher;
|
||||
private PrivdLauncher privdLauncher = null;
|
||||
private IPrivd privd = null;
|
||||
private boolean launchInProgress = false;
|
||||
|
||||
@@ -43,7 +46,6 @@ public class PrivdService extends Service {
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.d(TAG, "onCreate");
|
||||
privdLauncher = new RootPrivdLauncher(this);
|
||||
|
||||
tryLaunchPrivd();
|
||||
}
|
||||
@@ -54,7 +56,8 @@ public class PrivdService extends Service {
|
||||
Log.d(TAG, "onDestroy");
|
||||
synchronized (lock) {
|
||||
privd = null;
|
||||
privdLauncher.destroy();
|
||||
if (privdLauncher != null)
|
||||
privdLauncher.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +118,38 @@ public class PrivdService extends Service {
|
||||
if (launchInProgress) return;
|
||||
|
||||
try {
|
||||
privdLauncher.launchRoot();
|
||||
if (privdLauncher == null) {
|
||||
SettingsManager settings = new SettingsManager(this);
|
||||
|
||||
PrivilegeMode privilegeMode = settings.getPrivilegeMode();
|
||||
Log.d(TAG, "getting launcher for privilege mode: " + privilegeMode);
|
||||
privdLauncher = PrivdLauncher.createForPrivilegeMode(privilegeMode, this);
|
||||
if (privdLauncher == null)
|
||||
throw new IllegalStateException("privd not needed for current privilege mode");
|
||||
}
|
||||
|
||||
if (privdLauncher instanceof ShizukuPrivdLauncher shizukuPrivdLauncher) shizukuPrivdLauncher
|
||||
.checkShizukuPermission()
|
||||
.filter(r -> r)
|
||||
.ifPresentOrElse(
|
||||
r -> Log.i(TAG, "shizuku connected and permission granted"),
|
||||
() -> {
|
||||
Log.e(TAG, "shizuku not connected or permission denied");
|
||||
startActivity(new Intent(this, ConnectionRequestActivity.class)
|
||||
.setAction(ConnectionRequestActivity.INTENT_ACTION_REQUEST_SHIZUKU)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
}
|
||||
);
|
||||
|
||||
privdLauncher.setErrorListener(t -> {
|
||||
synchronized (lock) {
|
||||
if (isPrivdConnected() || !launchInProgress) return;
|
||||
launchInProgress = false;
|
||||
onPrivdLaunchFailure(t);
|
||||
}
|
||||
});
|
||||
|
||||
privdLauncher.launch();
|
||||
handler.postDelayed(() -> {
|
||||
synchronized (lock) {
|
||||
if (isPrivdConnected() || !launchInProgress) return;
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package io.benwiegand.projection.geargrinder.privileged;
|
||||
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_TOKEN_PATH;
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.TOKEN_LENGTH;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Random;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.settings.PrivilegeMode;
|
||||
import io.benwiegand.projection.geargrinder.util.ShellUtil;
|
||||
|
||||
public abstract class PrivdLauncher {
|
||||
private static final String TAG = PrivdLauncher.class.getSimpleName();
|
||||
|
||||
protected static final String PRIVD_FILE_NAME = "privd.jar";
|
||||
protected static final String LAUNCH_SCRIPT_FILE_NAME = "launch-privd.sh";
|
||||
public static final String TOKEN_FILE_NAME = "privd-token.bin";
|
||||
|
||||
protected final Context context;
|
||||
protected final File daemonFile;
|
||||
protected final File launchScriptFile;
|
||||
protected final File tokenFile;
|
||||
|
||||
private boolean init = false;
|
||||
protected byte[] token = null;
|
||||
|
||||
protected Consumer<Throwable> errorListener = null;
|
||||
|
||||
public PrivdLauncher(Context context, File daemonFile, File launchScriptFile, File tokenFile) {
|
||||
this.context = context;
|
||||
this.daemonFile = daemonFile;
|
||||
this.launchScriptFile = launchScriptFile;
|
||||
this.tokenFile = tokenFile;
|
||||
}
|
||||
|
||||
public abstract void destroy();
|
||||
|
||||
public abstract void launch() throws Throwable;
|
||||
|
||||
public void setErrorListener(Consumer<Throwable> errorListener) {
|
||||
this.errorListener = errorListener;
|
||||
}
|
||||
|
||||
protected void onError(Throwable throwable) {
|
||||
if (errorListener == null) return;
|
||||
errorListener.accept(throwable);
|
||||
}
|
||||
|
||||
protected void init() throws IOException {
|
||||
if (init) return;
|
||||
Log.i(TAG, "init privd launcher");
|
||||
copyDaemon();
|
||||
generateToken();
|
||||
generateScript();
|
||||
init = true;
|
||||
}
|
||||
|
||||
protected void copyDaemon() throws IOException {
|
||||
if (daemonFile.isFile()) {
|
||||
Log.d(TAG, "deleting " + daemonFile);
|
||||
if (!daemonFile.delete()) throw new IOException("failed to delete existing copy of the daemon");
|
||||
}
|
||||
|
||||
Log.v(TAG, "copying " + PRIVD_FILE_NAME + " to " + daemonFile);
|
||||
try (InputStream is = context.getAssets().open(PRIVD_FILE_NAME);
|
||||
FileOutputStream os = new FileOutputStream(daemonFile)) {
|
||||
int len;
|
||||
byte[] buffer = new byte[4096];
|
||||
while ((len = is.read(buffer)) >= 0)
|
||||
os.write(buffer, 0, len);
|
||||
}
|
||||
}
|
||||
|
||||
protected void generateToken() {
|
||||
try {
|
||||
byte[] newToken = new byte[TOKEN_LENGTH];
|
||||
|
||||
Log.d(TAG, "generating random " + newToken.length + " byte token");
|
||||
Random random = SecureRandom.getInstanceStrong();
|
||||
random.nextBytes(newToken);
|
||||
|
||||
Log.v(TAG, "saving token to " + tokenFile);
|
||||
try (FileOutputStream os = new FileOutputStream(tokenFile)) {
|
||||
os.write(newToken);
|
||||
}
|
||||
|
||||
token = newToken;
|
||||
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "failed to generate token", t);
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
}
|
||||
|
||||
private void generateScript() throws IOException {
|
||||
String script = """
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# generated vars
|
||||
""";
|
||||
|
||||
script += "INIT_JAR_PATH=" + ShellUtil.wrapSingleQuote(daemonFile.getAbsolutePath()) + "\n";
|
||||
script += "export " + ENV_TOKEN_PATH + "=" + ShellUtil.wrapSingleQuote(tokenFile.getAbsolutePath()) + "\n";
|
||||
|
||||
script += """
|
||||
# end generated vars
|
||||
|
||||
PRIVD_NAME=Geargrinder-privd
|
||||
EXEC_JAR_PATH="/data/local/tmp/geargrinder-privd.jar"
|
||||
|
||||
if ! [[ -f "$INIT_JAR_PATH" ]]; then
|
||||
echo "file not found: $INIT_JAR_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "preparing"
|
||||
rm -vf "$EXEC_JAR_PATH"
|
||||
cp -v "$INIT_JAR_PATH" "$EXEC_JAR_PATH"
|
||||
chown -v "`id -u`:`id -g`" "$EXEC_JAR_PATH"
|
||||
chmod -v 0400 "$EXEC_JAR_PATH"
|
||||
export CLASSPATH="$EXEC_JAR_PATH"
|
||||
|
||||
set +e
|
||||
|
||||
echo "launching $PRIVD_NAME as $USER (`id -u`)"
|
||||
app_process /system/bin --nice-name=$PRIVD_NAME io.benwiegand.projection.geargrinder.privd.Main
|
||||
exit_code=$?
|
||||
echo "app_process exited with code $exit_code"
|
||||
exit $exit_code
|
||||
""";
|
||||
|
||||
Log.v(TAG, "saving launch script to " + launchScriptFile);
|
||||
try (FileOutputStream os = new FileOutputStream(launchScriptFile)) {
|
||||
os.write(script.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] readToken() throws IOException {
|
||||
if (!tokenFile.isFile()) return null;
|
||||
|
||||
try (FileInputStream is = new FileInputStream(tokenFile)) {
|
||||
byte[] token = new byte[TOKEN_LENGTH];
|
||||
int offset = 0;
|
||||
int len;
|
||||
while (offset < TOKEN_LENGTH) {
|
||||
len = is.read(token, offset, TOKEN_LENGTH - offset);
|
||||
if (len < 0) {
|
||||
Log.e(TAG, "stored token too short (" + len + " / " + TOKEN_LENGTH + " bytes): " + tokenFile);
|
||||
return null;
|
||||
}
|
||||
|
||||
offset += len;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
public static PrivdLauncher createForPrivilegeMode(PrivilegeMode mode, Context context) {
|
||||
return switch (mode) {
|
||||
case NO_ROOT -> null;
|
||||
case SHIZUKU -> new ShizukuPrivdLauncher(context);
|
||||
case ROOT -> new RootPrivdLauncher(context);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
+32
-136
@@ -1,80 +1,53 @@
|
||||
package io.benwiegand.projection.geargrinder.privileged;
|
||||
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_TOKEN;
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_TOKEN_PATH;
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.TOKEN_LENGTH;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Random;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.util.ShellUtil;
|
||||
|
||||
public class RootPrivdLauncher {
|
||||
public class RootPrivdLauncher extends PrivdLauncher {
|
||||
private static final String TAG = RootPrivdLauncher.class.getSimpleName();
|
||||
|
||||
private static final String PRIVD_FILE_NAME = "privd.jar";
|
||||
private static final String LAUNCH_SCRIPT_FILE_NAME = "launch-privd.sh";
|
||||
public static final String TOKEN_FILE_NAME = "privd-token.bin";
|
||||
|
||||
private final Context context;
|
||||
private final File daemonFile;
|
||||
private final File launchScriptFile;
|
||||
private final File tokenFile;
|
||||
|
||||
private boolean init = false;
|
||||
private Process rootProcess = null;
|
||||
private byte[] token = null;
|
||||
|
||||
public RootPrivdLauncher(Context context) {
|
||||
this.context = context;
|
||||
daemonFile = context.getCodeCacheDir().toPath().resolve(PRIVD_FILE_NAME).toFile();
|
||||
launchScriptFile = context.getCodeCacheDir().toPath().resolve(LAUNCH_SCRIPT_FILE_NAME).toFile();
|
||||
tokenFile = context.getFilesDir().toPath().resolve(TOKEN_FILE_NAME).toFile();
|
||||
|
||||
// TODO: for launching non-root (less secure)
|
||||
// daemonFile = context.getExternalFilesDir(null).toPath().resolve(PRIVD_FILE_NAME).toFile();
|
||||
// launchScriptFile = context.getExternalFilesDir(null).toPath().resolve(LAUNCH_SCRIPT_FILE_NAME).toFile();
|
||||
// tokenFile = context.getExternalFilesDir(null).toPath().resolve(TOKEN_FILE_NAME).toFile();
|
||||
super(
|
||||
context,
|
||||
context.getCodeCacheDir().toPath().resolve(PRIVD_FILE_NAME).toFile(),
|
||||
context.getCodeCacheDir().toPath().resolve(LAUNCH_SCRIPT_FILE_NAME).toFile(),
|
||||
context.getFilesDir().toPath().resolve(TOKEN_FILE_NAME).toFile()
|
||||
);
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
killRootProcess();
|
||||
killProcess();
|
||||
}
|
||||
|
||||
private void init() throws IOException {
|
||||
Log.i(TAG, "init privd launcher");
|
||||
copyDaemon();
|
||||
generateToken();
|
||||
generateScript();
|
||||
init = true;
|
||||
}
|
||||
|
||||
public void launchRoot() throws IOException {
|
||||
if (!init) init();
|
||||
public void launch() throws IOException {
|
||||
init();
|
||||
|
||||
Log.i(TAG, "launching privd as root");
|
||||
killRootProcess();
|
||||
executeDaemonRoot();
|
||||
killProcess();
|
||||
executeDaemon();
|
||||
}
|
||||
|
||||
private void killRootProcess() {
|
||||
private void killProcess() {
|
||||
if (rootProcess == null) return;
|
||||
Log.d(TAG, "killing privd root process");
|
||||
rootProcess.destroyForcibly();
|
||||
rootProcess = null;
|
||||
}
|
||||
|
||||
private void executeDaemonRoot() throws IOException {
|
||||
private void executeDaemon() throws IOException {
|
||||
assert rootProcess == null;
|
||||
|
||||
String command = "sh " + ShellUtil.wrapSingleQuote(launchScriptFile.getAbsolutePath());
|
||||
@@ -85,108 +58,31 @@ public class RootPrivdLauncher {
|
||||
processBuilder.environment().put(ENV_TOKEN, Base64.encodeToString(token, Base64.NO_WRAP));
|
||||
|
||||
rootProcess = processBuilder.start();
|
||||
}
|
||||
|
||||
private void copyDaemon() throws IOException {
|
||||
if (daemonFile.isFile()) {
|
||||
Log.d(TAG, "deleting " + daemonFile);
|
||||
if (!daemonFile.delete()) throw new IOException("failed to delete existing copy of the daemon");
|
||||
}
|
||||
new Thread(() -> {
|
||||
try (InputStream is = rootProcess.getInputStream();
|
||||
InputStream es = rootProcess.getErrorStream()) {
|
||||
|
||||
Log.v(TAG, "copying " + PRIVD_FILE_NAME + " to " + daemonFile);
|
||||
try (InputStream is = context.getAssets().open(PRIVD_FILE_NAME);
|
||||
FileOutputStream os = new FileOutputStream(daemonFile)) {
|
||||
int len;
|
||||
byte[] buffer = new byte[4096];
|
||||
while ((len = is.read(buffer)) >= 0)
|
||||
os.write(buffer, 0, len);
|
||||
}
|
||||
}
|
||||
Reader outReader = new InputStreamReader(is, StandardCharsets.UTF_8);
|
||||
Reader errReader = new InputStreamReader(es, StandardCharsets.UTF_8);
|
||||
|
||||
private void generateToken() {
|
||||
try {
|
||||
byte[] newToken = new byte[TOKEN_LENGTH];
|
||||
char[] buffer = new char[2048];
|
||||
int len;
|
||||
|
||||
Log.d(TAG, "generating random " + newToken.length + " byte token");
|
||||
Random random = SecureRandom.getInstanceStrong();
|
||||
random.nextBytes(newToken);
|
||||
while (rootProcess.isAlive()) {
|
||||
while ((len = outReader.read(buffer)) > 0)
|
||||
Log.v(TAG, "STDOUT: " + new String(buffer, 0, len));
|
||||
|
||||
Log.v(TAG, "saving token to " + tokenFile);
|
||||
try (FileOutputStream os = new FileOutputStream(tokenFile)) {
|
||||
os.write(newToken);
|
||||
}
|
||||
while ((len = errReader.read(buffer)) > 0)
|
||||
Log.e(TAG, "STDERR: " + new String(buffer, 0, len));
|
||||
|
||||
token = newToken;
|
||||
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "failed to generate token", t);
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
}
|
||||
|
||||
private void generateScript() throws IOException {
|
||||
String script = """
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# generated vars
|
||||
""";
|
||||
|
||||
script += "INIT_JAR_PATH=" + ShellUtil.wrapSingleQuote(daemonFile.getAbsolutePath()) + "\n";
|
||||
script += "export " + ENV_TOKEN_PATH + "=" + ShellUtil.wrapSingleQuote(tokenFile.getAbsolutePath()) + "\n";
|
||||
|
||||
script += """
|
||||
# end generated vars
|
||||
|
||||
PRIVD_NAME=Geargrinder-privd
|
||||
EXEC_JAR_PATH="/data/local/tmp/geargrinder-privd.jar"
|
||||
|
||||
if ! [[ -f "$INIT_JAR_PATH" ]]; then
|
||||
echo "file not found: $INIT_JAR_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp -v "$INIT_JAR_PATH" "$EXEC_JAR_PATH"
|
||||
chown -v "`id -u`:`id -g`" "$EXEC_JAR_PATH"
|
||||
chmod -v 0400 "$EXEC_JAR_PATH"
|
||||
export CLASSPATH="$EXEC_JAR_PATH"
|
||||
|
||||
set +e
|
||||
|
||||
echo "launching $PRIVD_NAME as $USER (`id -u`)"
|
||||
app_process /system/bin --nice-name=$PRIVD_NAME io.benwiegand.projection.geargrinder.privd.Main
|
||||
exit_code=$?
|
||||
echo "app_process exited with code $exit_code"
|
||||
exit $exit_code
|
||||
""";
|
||||
|
||||
Log.v(TAG, "saving launch script to " + launchScriptFile);
|
||||
try (FileOutputStream os = new FileOutputStream(launchScriptFile)) {
|
||||
os.write(script.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] readToken(Context context) throws IOException {
|
||||
File tokenFile = context.getFilesDir().toPath().resolve(TOKEN_FILE_NAME).toFile();
|
||||
if (!tokenFile.isFile()) return null;
|
||||
|
||||
try (FileInputStream is = new FileInputStream(tokenFile)) {
|
||||
byte[] token = new byte[TOKEN_LENGTH];
|
||||
int offset = 0;
|
||||
int len;
|
||||
while (offset < TOKEN_LENGTH) {
|
||||
len = is.read(token, offset, TOKEN_LENGTH - offset);
|
||||
if (len < 0) {
|
||||
Log.e(TAG, "stored token too short (" + len + " / " + TOKEN_LENGTH + " bytes): " + tokenFile);
|
||||
return null;
|
||||
}
|
||||
|
||||
offset += len;
|
||||
Log.e(TAG, "TERMINATED: " + rootProcess.exitValue());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "root process loop threw", e);
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package io.benwiegand.projection.geargrinder.privileged;
|
||||
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_TOKEN;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.IShizukuUserService;
|
||||
import io.benwiegand.projection.geargrinder.service.GeargrinderServiceConnector;
|
||||
import moe.shizuku.server.IShizukuService;
|
||||
import rikka.shizuku.Shizuku;
|
||||
|
||||
public class ShizukuPrivdLauncher extends PrivdLauncher implements GeargrinderServiceConnector.ConnectionListener {
|
||||
private static final String TAG = ShizukuPrivdLauncher.class.getSimpleName();
|
||||
|
||||
private static final long SHIZUKU_CONNECTION_TIMEOUT = 10000;
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
private final GeargrinderServiceConnector connector;
|
||||
|
||||
private static File requireExternalFilesDir(Context context) {
|
||||
File file = context.getExternalFilesDir(null);
|
||||
if (file == null) throw new UnsupportedOperationException("an external files directory is required");
|
||||
return file;
|
||||
}
|
||||
|
||||
public ShizukuPrivdLauncher(Context context) {
|
||||
super(
|
||||
context,
|
||||
requireExternalFilesDir(context).toPath().resolve(PRIVD_FILE_NAME).toFile(),
|
||||
requireExternalFilesDir(context).toPath().resolve(LAUNCH_SCRIPT_FILE_NAME).toFile(),
|
||||
requireExternalFilesDir(context).toPath().resolve(TOKEN_FILE_NAME).toFile()
|
||||
);
|
||||
|
||||
connector = new GeargrinderServiceConnector(TAG, context, this);
|
||||
Shizuku.addBinderReceivedListenerSticky(this::onBinderReceived);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
Shizuku.removeBinderReceivedListener(this::onBinderReceived);
|
||||
connector.getShizukuUserService().ifPresent(this::killProcess);
|
||||
connector.destroy();
|
||||
}
|
||||
|
||||
private void onBinderReceived() {
|
||||
Log.i(TAG, "Shizuku binder received");
|
||||
checkShizukuPermission()
|
||||
.filter(p -> p)
|
||||
.ifPresent(permission -> connector.bindShizukuUserService());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShizukuUserServiceConnected(IShizukuUserService service) {
|
||||
Log.i(TAG, "Shizuku user service connected");
|
||||
synchronized (lock) {
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private IShizukuUserService waitForShizukuUserService() throws InterruptedException, TimeoutException {
|
||||
synchronized (lock) {
|
||||
Optional<IShizukuUserService> serviceOptional = connector.getShizukuUserService();
|
||||
if (serviceOptional.isPresent()) return serviceOptional.get();
|
||||
|
||||
Log.i(TAG, "waiting for Shizuku user service");
|
||||
lock.wait(SHIZUKU_CONNECTION_TIMEOUT);
|
||||
serviceOptional = connector.getShizukuUserService();
|
||||
|
||||
return serviceOptional
|
||||
.orElseThrow(() -> new TimeoutException("Shizuku user service not connected before timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch() throws IOException, RemoteException, InterruptedException, TimeoutException {
|
||||
init();
|
||||
|
||||
Log.i(TAG, "launching privd as shizuku");
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
IShizukuUserService userService = waitForShizukuUserService();
|
||||
Log.i(TAG, "shizuku connected");
|
||||
Log.d(TAG, "shizuku version: " + Shizuku.getVersion());
|
||||
Log.d(TAG, "shizuku UID: " + Shizuku.getUid());
|
||||
Log.d(TAG, "SELinux context: " + Shizuku.getSELinuxContext());
|
||||
executeDaemon(userService);
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "shizuku privd launch failed", t);
|
||||
onError(t);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void executeDaemon(IShizukuUserService userService) throws RemoteException {
|
||||
Log.v(TAG, "requesting privd execution");
|
||||
userService.execPrivd(launchScriptFile.getAbsolutePath(), Map.of(
|
||||
ENV_TOKEN, Base64.encodeToString(token, Base64.NO_WRAP)
|
||||
));
|
||||
}
|
||||
|
||||
private void killProcess(IShizukuUserService userService) {
|
||||
Log.d(TAG, "requesting to kill privd shizuku process");
|
||||
try {
|
||||
userService.killPrivd();
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "failed to kill process", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<IShizukuService> getShizukuService() {
|
||||
return Optional.ofNullable(IShizukuService.Stub.asInterface(Shizuku.getBinder()));
|
||||
}
|
||||
|
||||
public Optional<Boolean> checkShizukuPermission() {
|
||||
return getShizukuService()
|
||||
.flatMap(shizuku -> {
|
||||
try {
|
||||
return Optional.of(shizuku.checkSelfPermission());
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "remote exception while checking shizuku permission", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package io.benwiegand.projection.geargrinder.privileged;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.IShizukuUserService;
|
||||
|
||||
public class ShizukuUserService extends IShizukuUserService.Stub {
|
||||
private static final String TAG = ShizukuUserService.class.getSimpleName();
|
||||
|
||||
private Process privdProcess = null;
|
||||
|
||||
@Override
|
||||
public void execPrivd(String scriptPath, Map<String, String> env) throws RemoteException {
|
||||
if (privdProcess != null) killPrivd();
|
||||
|
||||
try {
|
||||
Log.v(TAG, "executing privd launcher script at: " + scriptPath);
|
||||
ProcessBuilder builder = new ProcessBuilder("sh", scriptPath);
|
||||
builder.environment().putAll(env);
|
||||
privdProcess = builder.start();
|
||||
|
||||
new Thread(() -> {
|
||||
try (InputStream is = privdProcess.getInputStream();
|
||||
InputStream es = privdProcess.getErrorStream()) {
|
||||
|
||||
Reader outReader = new InputStreamReader(is, StandardCharsets.UTF_8);
|
||||
Reader errReader = new InputStreamReader(es, StandardCharsets.UTF_8);
|
||||
|
||||
char[] buffer = new char[2048];
|
||||
int len;
|
||||
|
||||
while (privdProcess.isAlive()) {
|
||||
while ((len = outReader.read(buffer)) > 0)
|
||||
Log.v(TAG, "STDOUT: " + new String(buffer, 0, len));
|
||||
|
||||
while ((len = errReader.read(buffer)) > 0)
|
||||
Log.e(TAG, "STDERR: " + new String(buffer, 0, len));
|
||||
|
||||
}
|
||||
|
||||
Log.e(TAG, "TERMINATED: " + privdProcess.exitValue());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "shizuku process loop threw", e);
|
||||
}
|
||||
}).start();
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "failed to start privd");
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void killPrivd() throws RemoteException {
|
||||
if (privdProcess == null) return;
|
||||
if (!privdProcess.isAlive()) {
|
||||
privdProcess = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "killing existing privd process");
|
||||
privdProcess.destroyForcibly();
|
||||
privdProcess = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws RemoteException {
|
||||
killPrivd();
|
||||
}
|
||||
}
|
||||
+29
-1
@@ -12,10 +12,13 @@ import java.util.Optional;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.AccessibilityInputService;
|
||||
import io.benwiegand.projection.geargrinder.ConnectionService;
|
||||
import io.benwiegand.projection.geargrinder.IShizukuUserService;
|
||||
import io.benwiegand.projection.geargrinder.PackageService;
|
||||
import io.benwiegand.projection.geargrinder.PrivdService;
|
||||
import io.benwiegand.projection.geargrinder.ProjectionActivity;
|
||||
import io.benwiegand.projection.geargrinder.makeshiftbind.MakeshiftServiceConnection;
|
||||
import io.benwiegand.projection.geargrinder.privileged.ShizukuUserService;
|
||||
import rikka.shizuku.Shizuku;
|
||||
|
||||
public class GeargrinderServiceConnector extends MakeshiftServiceConnection {
|
||||
private static final String PACKAGE_NAME = "io.benwiegand.projection.geargrinder";
|
||||
@@ -25,18 +28,28 @@ public class GeargrinderServiceConnector extends MakeshiftServiceConnection {
|
||||
private static final ComponentName PRIVD_SERVICE_COMPONENT = new ComponentName(PACKAGE_NAME, PrivdService.class.getName());
|
||||
private static final ComponentName PACKAGE_SERVICE_COMPONENT = new ComponentName(PACKAGE_NAME, PackageService.class.getName());
|
||||
private static final ComponentName CONNECTION_SERVICE_COMPONENT = new ComponentName(PACKAGE_NAME, ConnectionService.class.getName());
|
||||
private static final ComponentName SHIZUKU_USER_SERVICE_COMPONENT = new ComponentName(PACKAGE_NAME, ShizukuUserService.class.getName());
|
||||
|
||||
private static final Shizuku.UserServiceArgs SHIZUKU_ARGS = new Shizuku.UserServiceArgs(SHIZUKU_USER_SERVICE_COMPONENT)
|
||||
.tag("geargrinder-shizuku-service")
|
||||
.processNameSuffix("shizuku-service")
|
||||
.daemon(false);
|
||||
|
||||
private final Map<ComponentName, IBinder> binderMap = new HashMap<>();
|
||||
private final String tag;
|
||||
private final Context context;
|
||||
private final ConnectionListener listener;
|
||||
|
||||
private boolean shizukuBound = false;
|
||||
private boolean contextBound = false;
|
||||
|
||||
public interface ConnectionListener {
|
||||
default void onAccessibilityServiceConnected(AccessibilityInputService.ServiceBinder binder) {}
|
||||
default void onProjectionActivityConnected(ProjectionActivity.ActivityBinder binder) {}
|
||||
default void onPrivdServiceConnected(PrivdService.ServiceBinder binder) {}
|
||||
default void onPackageServiceConnected(PackageService.ServiceBinder binder) {}
|
||||
default void onConnectionServiceConnected(ConnectionService.ServiceBinder binder) {}
|
||||
default void onShizukuUserServiceConnected(IShizukuUserService service) {}
|
||||
}
|
||||
|
||||
public GeargrinderServiceConnector(String tag, Context context, ConnectionListener listener) {
|
||||
@@ -48,7 +61,9 @@ public class GeargrinderServiceConnector extends MakeshiftServiceConnection {
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
context.unbindService(this);
|
||||
if (contextBound) context.unbindService(this);
|
||||
if (shizukuBound && Shizuku.getBinder() != null)
|
||||
Shizuku.unbindUserService(SHIZUKU_ARGS, this, true);
|
||||
}
|
||||
|
||||
private void makeshiftBind(ComponentName component) {
|
||||
@@ -57,6 +72,7 @@ public class GeargrinderServiceConnector extends MakeshiftServiceConnection {
|
||||
|
||||
private void realBind(ComponentName component, int flags) {
|
||||
context.bindService(new Intent().setComponent(component), this, flags);
|
||||
contextBound = true;
|
||||
}
|
||||
|
||||
private Optional<IBinder> getBinder(ComponentName componentName) {
|
||||
@@ -109,6 +125,16 @@ public class GeargrinderServiceConnector extends MakeshiftServiceConnection {
|
||||
.map(b -> (ConnectionService.ServiceBinder) b);
|
||||
}
|
||||
|
||||
public void bindShizukuUserService() {
|
||||
Shizuku.bindUserService(SHIZUKU_ARGS, this);
|
||||
shizukuBound = true;
|
||||
}
|
||||
|
||||
public Optional<IShizukuUserService> getShizukuUserService() {
|
||||
return getBinder(SHIZUKU_USER_SERVICE_COMPONENT)
|
||||
.map(IShizukuUserService.Stub::asInterface);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
@@ -124,6 +150,8 @@ public class GeargrinderServiceConnector extends MakeshiftServiceConnection {
|
||||
listener.onPackageServiceConnected((PackageService.ServiceBinder) service);
|
||||
} else if (name.equals(CONNECTION_SERVICE_COMPONENT)) {
|
||||
listener.onConnectionServiceConnected((ConnectionService.ServiceBinder) service);
|
||||
} else if (name.equals(SHIZUKU_USER_SERVICE_COMPONENT)) {
|
||||
listener.onShizukuUserServiceConnected(IShizukuUserService.Stub.asInterface(service));
|
||||
} else {
|
||||
Log.wtf(tag, "unhandled component: " + name);
|
||||
assert false;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package io.benwiegand.projection.geargrinder.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.R;
|
||||
|
||||
public enum OperationalMode {
|
||||
AUDIO_ONLY,
|
||||
SCREEN_MIRRORING,
|
||||
GEARGRINDER_PROJECTION;
|
||||
|
||||
public static OperationalMode read(Context context, SharedPreferences prefs) {
|
||||
return SettingsManager.enumForPref(
|
||||
context, prefs,
|
||||
R.string.key_operational_mode,
|
||||
R.string.operational_mode_screen_mirroring,
|
||||
List.of(
|
||||
Pair.create(R.string.operational_mode_audio_only, AUDIO_ONLY),
|
||||
Pair.create(R.string.operational_mode_screen_mirroring, SCREEN_MIRRORING),
|
||||
Pair.create(R.string.operational_mode_geargrinder_projection, GEARGRINDER_PROJECTION)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package io.benwiegand.projection.geargrinder.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.R;
|
||||
|
||||
public enum PrivilegeMode {
|
||||
NO_ROOT,
|
||||
SHIZUKU,
|
||||
ROOT;
|
||||
|
||||
public static PrivilegeMode read(Context context, SharedPreferences prefs) {
|
||||
return SettingsManager.enumForPref(
|
||||
context, prefs,
|
||||
R.string.key_privilege_mode,
|
||||
R.string.privilege_mode_no_root,
|
||||
List.of(
|
||||
Pair.create(R.string.privilege_mode_no_root, NO_ROOT),
|
||||
Pair.create(R.string.privilege_mode_shizuku, SHIZUKU),
|
||||
Pair.create(R.string.privilege_mode_root, ROOT)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package io.benwiegand.projection.geargrinder.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SettingsManager {
|
||||
private static final String TAG = SettingsManager.class.getSimpleName();
|
||||
private static final String PREFERENCE_NAME = "io.benwiegand.projection.geargrinder_preferences";
|
||||
|
||||
private final Context context;
|
||||
private final SharedPreferences prefs;
|
||||
|
||||
public SettingsManager(Context context) {
|
||||
this.context = context;
|
||||
prefs = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public OperationalMode getOperationalMode() {
|
||||
return OperationalMode.read(context, prefs);
|
||||
}
|
||||
|
||||
public PrivilegeMode getPrivilegeMode() {
|
||||
return PrivilegeMode.read(context, prefs);
|
||||
}
|
||||
|
||||
public static <T> T enumForPref(Context context, SharedPreferences prefs, int key, int defaultValue, List<Pair<Integer, T>> mapping) {
|
||||
String value = prefs.getString(
|
||||
context.getString(key),
|
||||
context.getString(defaultValue));
|
||||
|
||||
T defaultMapping = null;
|
||||
for (Pair<Integer, T> entry : mapping) {
|
||||
if (entry.first == defaultValue) defaultMapping = entry.second;
|
||||
if (!context.getString(entry.first).equals(value)) continue;
|
||||
return entry.second;
|
||||
}
|
||||
|
||||
if (defaultMapping == null)
|
||||
Log.wtf(TAG, "default value not present in mappings");
|
||||
|
||||
Log.wtf(TAG, "unhandled value for pref " + context.getString(key) + ": " + value);
|
||||
assert false;
|
||||
return defaultMapping;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string-array name="privilege_mode_entries">
|
||||
<!-- <item>no root</item>-->
|
||||
<item>Shizuku</item>
|
||||
<item>root</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="operational_mode_entries">
|
||||
<item>audio only</item>
|
||||
<item>screen mirroring</item>
|
||||
<item>Geargrinder UI</item>
|
||||
</string-array>
|
||||
|
||||
<string-array translatable="false" name="privilege_mode_values">
|
||||
<!-- <item>@string/privilege_mode_no_root</item>-->
|
||||
<item>@string/privilege_mode_shizuku</item>
|
||||
<item>@string/privilege_mode_root</item>
|
||||
</string-array>
|
||||
|
||||
<string-array translatable="false" name="operational_mode_values">
|
||||
<item>@string/operational_mode_audio_only</item>
|
||||
<item>@string/operational_mode_screen_mirroring</item>
|
||||
<item>@string/operational_mode_geargrinder_projection</item>
|
||||
</string-array>
|
||||
|
||||
<string translatable="false" name="operational_mode_audio_only">audio_only</string>
|
||||
<string translatable="false" name="operational_mode_screen_mirroring">screen_mirroring</string>
|
||||
<string translatable="false" name="operational_mode_geargrinder_projection">geargrinder_projection</string>
|
||||
|
||||
<string translatable="false" name="privilege_mode_no_root">no_root</string>
|
||||
<string translatable="false" name="privilege_mode_shizuku">shizuku</string>
|
||||
<string translatable="false" name="privilege_mode_root">root</string>
|
||||
|
||||
<string translatable="false" name="key_privilege_mode">privilege_mode</string>
|
||||
<string translatable="false" name="key_operational_mode">operational_mode</string>
|
||||
</resources>
|
||||
@@ -37,5 +37,11 @@
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="debug_title">Debug</string>
|
||||
<string name="connect_your_car_over_usb">Connect your car over USB to start Geargrinder.</string>
|
||||
<string name="privilege_mode_pref_name">Privilege mode</string>
|
||||
<string name="operational_mode_pref_name">Operational mode</string>
|
||||
<string name="shizuku_permission_request">Shizuku permission</string>
|
||||
<string name="shizuku_permission_rationale">Shizuku is used to access system APIs for a better user experience. Things like creating trusted virtual displays, injecting input events into said displays, etc.</string>
|
||||
<string name="shizuku_not_running">Unable to connect to Shizuku, is Shizuku running?</string>
|
||||
<string name="shizuku_too_old">Please update Shizuku.</string>
|
||||
|
||||
</resources>
|
||||
@@ -2,6 +2,35 @@
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceCategory
|
||||
app:title="operations"
|
||||
app:iconSpaceReserved="false"
|
||||
>
|
||||
|
||||
<!-- TODO: set to no_root by default when that's implemented -->
|
||||
<ListPreference
|
||||
app:key="@string/key_privilege_mode"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/privilege_mode_pref_name"
|
||||
app:entries="@array/privilege_mode_entries"
|
||||
app:entryValues="@array/privilege_mode_values"
|
||||
app:defaultValue="@string/privilege_mode_root"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
/>
|
||||
|
||||
<!-- TODO: implement -->
|
||||
<ListPreference
|
||||
app:key="@string/key_operational_mode"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/operational_mode_pref_name"
|
||||
app:entries="@array/operational_mode_entries"
|
||||
app:entryValues="@array/operational_mode_values"
|
||||
app:defaultValue="@string/operational_mode_screen_mirroring"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:isPreferenceVisible="false"
|
||||
/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -8,6 +8,7 @@ material = "1.13.0"
|
||||
bouncycastle = "1.45"
|
||||
localbroadcastmanager = "1.1.0"
|
||||
preference = "1.2.1"
|
||||
shizuku = "13.1.5"
|
||||
|
||||
[libraries]
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
@@ -18,6 +19,8 @@ material = { group = "com.google.android.material", name = "material", version.r
|
||||
bouncycastle = { group = "org.bouncycastle", name = "bcprov-jdk16", version.ref = "bouncycastle" }
|
||||
localbroadcastmanager = { group = "androidx.localbroadcastmanager", name = "localbroadcastmanager", version.ref = "localbroadcastmanager" }
|
||||
preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }
|
||||
shizuku-api = { group = "dev.rikka.shizuku", name = "api", version.ref = "shizuku" }
|
||||
shizuku-provider = { group = "dev.rikka.shizuku", name = "provider", version.ref = "shizuku" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user