Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
802ae36
feat(root): refactor root access
researchxxl Jan 10, 2026
5d03d39
readd imports dropped in e778efb4d6e0fc30f4ff8fabeb8c0567537a66c7
researchxxl Jan 10, 2026
7ab68db
Merge branch 'main' into refactorRoot
researchxxl Jan 20, 2026
68de8d3
Merge branch 'main' into refactorRoot
researchxxl Jan 31, 2026
cef2e2f
Merge branch 'main' into refactorRoot
researchxxl Feb 4, 2026
43d3200
Merge branch 'main' into refactorRoot
researchxxl Feb 6, 2026
77f6283
Merge branch 'main' into refactorRoot
researchxxl Feb 8, 2026
f5697d0
Merge branch 'main' into refactorRoot
researchxxl Feb 9, 2026
0b804f8
Merge branch 'main' into refactorRoot
researchxxl Feb 12, 2026
5bafb68
Merge branch 'main' into refactorRoot
researchxxl Feb 14, 2026
cdbfc52
Merge branch 'main' into refactorRoot
researchxxl Feb 14, 2026
5694aff
Merge branch 'main' into refactorRoot
researchxxl Feb 23, 2026
6b8d6e4
Resolve conflict with main, add use root preference to new settings s…
dbhavsar76 Feb 23, 2026
bbdbafc
Merge branch 'main' into refactorRoot
researchxxl Feb 23, 2026
a787407
Merge branch 'main' into refactorRoot
researchxxl Feb 23, 2026
59d7dd4
restart syncthing after enabling/disabling root (#173)
dbhavsar76 Feb 26, 2026
bfc6b85
Merge branch 'main' into refactorRoot
researchxxl Feb 26, 2026
d8014fd
Merge branch 'main' into refactorRoot
researchxxl Mar 6, 2026
6cfd409
Merge branch 'main' into refactorRoot
researchxxl Mar 6, 2026
c8a3d7a
Merge branch 'main' into refactorRoot
researchxxl Mar 8, 2026
5583f1c
Merge branch 'main' into refactorRoot
researchxxl Mar 10, 2026
7e80aac
Merge branch 'main' into refactorRoot
researchxxl Mar 11, 2026
e4e8dab
Merge branch 'main' into refactorRoot
researchxxl Mar 17, 2026
69f216c
Merge branch 'main' into refactorRoot
researchxxl Mar 17, 2026
8e86422
Merge remote-tracking branch 'origin/main' into refactorRoot
dbhavsar76 Apr 4, 2026
b9cad51
Merge branch 'main' into refactorRoot
researchxxl Apr 17, 2026
a32e4c6
Merge branch 'main' into refactorRoot
researchxxl Apr 27, 2026
60c557f
abstract all the libsuperuser calls to new RootAccess adapter
dbhavsar76 May 5, 2026
e715ce5
resolve conflicts
dbhavsar76 May 5, 2026
79eddd4
Merge branch 'main' into refactorRoot
researchxxl May 18, 2026
108ff5c
java.io.DataOutputStream
researchxxl May 18, 2026
37cd13e
Merge branch 'main' into refactorRoot
researchxxl May 18, 2026
a2a0005
Merge branch 'main' into refactorRoot
researchxxl May 20, 2026
36f61df
Merge branch 'main' into refactorRoot
researchxxl May 26, 2026
025b206
Merge branch 'main' into refactorRoot
researchxxl Jun 8, 2026
d2e9cba
Merge branch 'main' into refactorRoot
researchxxl Jun 12, 2026
40d41fa
Merge branch 'main' into refactorRoot
researchxxl Jun 12, 2026
c7b2a9a
Update SettingsBehaviorScreen.kt
researchxxl Jun 12, 2026
97cdb0b
Merge branch 'main' into refactorRoot
researchxxl Jun 12, 2026
1be472f
Merge branch 'main' into refactorRoot
researchxxl Jun 17, 2026
de323ec
Merge branch 'main' into refactorRoot
researchxxl Jun 17, 2026
4b6dc20
Merge branch 'main' into refactorRoot
researchxxl Jun 29, 2026
4c65155
Merge branch 'main' into refactorRoot
researchxxl Jul 1, 2026
48ddedc
Merge branch 'main' into refactorRoot
researchxxl Jul 8, 2026
95dc859
Merge branch 'main' into refactorRoot
researchxxl Jul 22, 2026
fb0b138
Merge branch 'main' into refactorRoot
researchxxl Jul 29, 2026
ee19dac
Merge branch 'main' into refactorRoot
researchxxl Aug 4, 2026
be5253e
fix bootreceiver
researchxxl Aug 4, 2026
db3f0b9
util: add useroot to new funcs
researchxxl Aug 4, 2026
08a7d37
fix restapi
researchxxl Aug 4, 2026
40788a7
add preference.java
researchxxl Aug 4, 2026
8ec299e
fix syncthingservice
researchxxl Aug 4, 2026
18295cb
fix conflict detector not working in root mode
researchxxl Aug 4, 2026
5259afc
Apply suggestions from code review
researchxxl Aug 4, 2026
523228c
preference into appprefs
researchxxl Aug 4, 2026
fe3a6f6
simplify util
researchxxl Aug 4, 2026
2089e8f
Merge branch 'main' into refactorRoot
researchxxl Aug 4, 2026
71e424d
Merge branch 'main' into refactorRoot
researchxxl Aug 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {
implementation(libs.guava)
implementation(libs.jbcrypt)
implementation(libs.kotlinx.serialization.core)
implementation(libs.libsuperuser)
implementation(libs.lingala.zip4j)
implementation(libs.localbroadcastmanager)
implementation(libs.navigation3.runtime)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import com.nutomic.syncthingandroid.service.AppPrefs;
import com.nutomic.syncthingandroid.service.Constants;
import com.nutomic.syncthingandroid.service.SyncthingService;
import com.nutomic.syncthingandroid.root.RootAccess;
import com.nutomic.syncthingandroid.util.Util;

import java.lang.SecurityException;

Expand All @@ -28,6 +30,16 @@ public void onReceive(Context context, Intent intent) {
return;
}

if (packageReplaced) {
if (AppPrefs.getUseRoot(context) && RootAccess.isRootAvailableBlocking()) {
/**
* In Root mode, there will be a SyncthingNative process left running after app update.
*/
Log.d(TAG, "ACTION_MY_PACKAGE_REPLACED: Killing leftover SyncthingNative instance if present ...");
Util.killProcess(Constants.FILENAME_SYNCTHING_BINARY, true);
}
}

// Check if we should (re)start now.
if (!AppPrefs.getStartServiceOnBoot(context)) {
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.nutomic.syncthingandroid.root;

import eu.chainfire.libsuperuser.Shell;

public final class RootAccess {

private RootAccess() {
}

public static boolean isRootAvailableBlocking() {
return Shell.SU.available();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@ public static final boolean getStartServiceOnBoot(Context context) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
return sp.getBoolean(Constants.PREF_START_SERVICE_ON_BOOT, false);
}

public static final boolean getUseRoot(Context context) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
return sp.getBoolean(Constants.PREF_USE_ROOT, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ private PowerSource() { }
// Preferences - Behaviour
public static final String PREF_START_SERVICE_ON_BOOT = "always_run_in_background";
public static final String PREF_BROADCAST_SERVICE_CONTROL = "broadcast_service_control";
public static final String PREF_USE_ROOT = "use_root";
public static final String PREF_ALLOW_OVERWRITE_FILES = "allow_overwrite_files";

// Preferences - Syncthing Options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ public void sendConfig() {
public void shutdown() {
hasShutdown = true;
executorService.shutdownNow();
Util.killProcess("find");
Util.killProcess("find", AppPrefs.getUseRoot(mContext));
new PostRequest(mContext, mUrl, PostRequest.URI_SYSTEM_SHUTDOWN, mApiKey,
null, null, null);
}
Expand Down Expand Up @@ -1252,7 +1252,7 @@ public void setRemoteCompletionInfo(final String deviceId,
// Check for ".sync-conflict-YYYYMMDD-HHMMSS-DEVICEI*" files.
mLocalCompletion.setDiscoveredConflictFiles(
folderId,
Util.getSyncConflictFiles(folder.path)
Util.getSyncConflictFiles(folder.path, AppPrefs.getUseRoot(mContext))
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
import com.google.common.io.Files;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.SyncthingApp;
import com.nutomic.syncthingandroid.root.RootAccess;
import com.nutomic.syncthingandroid.util.FileUtils;
import com.nutomic.syncthingandroid.util.Util;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
Expand Down Expand Up @@ -59,6 +61,7 @@ public class SyncthingRunnable implements Runnable {
private final File mSyncthingBinary;
private String[] mCommand;
private final File mSyncthingLogFile;
private final boolean mUseRoot;

@Inject
SharedPreferences mPreferences;
Expand Down Expand Up @@ -88,6 +91,7 @@ public SyncthingRunnable(Context context, Command command) {
mSyncthingLogFile = Constants.getSyncthingLogFile(mContext);

// Get preferences relevant to starting syncthing core.
mUseRoot = mPreferences.getBoolean(Constants.PREF_USE_ROOT, false) && RootAccess.isRootAvailableBlocking();
switch (command) {
case deviceid:
mCommand = new String[]{mSyncthingBinary.getPath(), "device-id"};
Expand Down Expand Up @@ -436,9 +440,30 @@ private Process setupAndLaunch(HashMap<String, String> env) throws IOException,
throw new ExecutableNotFoundException(mCommand[0]);
}
}
ProcessBuilder pb = new ProcessBuilder(mCommand);
pb.environment().putAll(env);
return pb.start();

if (mUseRoot) {
ProcessBuilder pb = new ProcessBuilder("su");
Process process = pb.start();
// The su binary prohibits the inheritance of environment variables.
// Even with --preserve-environment the environment gets messed up.
// We therefore start a root shell, and set all the environment variables manually.
DataOutputStream suOut = new DataOutputStream(process.getOutputStream());
for (Map.Entry<String, String> entry : env.entrySet()) {
suOut.writeBytes(String.format("export %s=\"%s\"\n", entry.getKey(), entry.getValue()));
}
suOut.flush();
// Exec will replace the su process image by Syncthing as execlp in C does.
// Without using exec, the process will drop to the root shell as soon as Syncthing terminates like a normal shell does.
// If we did not use exec, we would wait infinitely for the process to terminate (ret = process.waitFor(); in run()).
// With exec the whole process terminates when Syncthing exits.
suOut.writeBytes("exec " + TextUtils.join(" ", mCommand) + "\n");
suOut.flush();
Comment thread
researchxxl marked this conversation as resolved.
return process;
} else {
ProcessBuilder pb = new ProcessBuilder(mCommand);
pb.environment().putAll(env);
return pb.start();
}
}

public class ExecutableNotFoundException extends Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ private void launchStartupTask(SyncthingRunnable.Command srCommand) {
* Check if an old syncthing instance is still running.
* This happens after an in-place app upgrade. If so, end it.
*/
Util.killProcess(Constants.FILENAME_SYNCTHING_BINARY);
Util.killProcess(Constants.FILENAME_SYNCTHING_BINARY, AppPrefs.getUseRoot(this));

// Start the syncthing binary in a separate thread.
Thread.UncaughtExceptionHandler syncthingRunnableThreadExceptionHandler = new Thread.UncaughtExceptionHandler() {
Expand Down Expand Up @@ -712,7 +712,7 @@ private void shutdown(State newState) {
}

if (mSyncthingRunnable != null) {
Util.killProcess(Constants.FILENAME_SYNCTHING_BINARY);
Util.killProcess(Constants.FILENAME_SYNCTHING_BINARY, AppPrefs.getUseRoot(this));
if (mSyncthingRunnableThread != null) {
LogV("Waiting for mSyncthingRunnableThread to finish after killProcess(Syncthing) ...");
try {
Expand Down Expand Up @@ -1383,7 +1383,6 @@ private boolean importConfigSharedPrefs(final File file) {
case "pref_current_language":
case "restartOnWakeup":
case "wakelock_while_binary_running":
case "use_root":
case "important_news_shown_version":
LogV("importConfig: Ignoring deprecated pref \"" + prefKey + "\".");
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
package com.nutomic.syncthingandroid.settings

import android.content.Intent
import android.widget.Toast
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.navigation3.runtime.EntryProviderScope
import com.nutomic.syncthingandroid.R
import com.nutomic.syncthingandroid.root.RootAccess
import com.nutomic.syncthingandroid.service.Constants
import com.nutomic.syncthingandroid.service.SyncthingService
import com.nutomic.syncthingandroid.util.LocalActivityScope
import com.nutomic.syncthingandroid.util.Util
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.zhanghai.compose.preference.SwitchPreference
import me.zhanghai.compose.preference.rememberPreferenceState

Expand All @@ -19,10 +29,48 @@ fun EntryProviderScope<SettingsRoute>.settingsBehaviorEntry() {

@Composable
fun SettingsBehaviorScreen() {
val context = LocalContext.current
val scope = LocalActivityScope.current

val autoStart = rememberPreferenceState(Constants.PREF_START_SERVICE_ON_BOOT, false)
val broadcast = rememberPreferenceState(Constants.PREF_BROADCAST_SERVICE_CONTROL, false)
val overwrite = rememberPreferenceState(Constants.PREF_ALLOW_OVERWRITE_FILES, false)
val useRoot = rememberPreferenceState(Constants.PREF_USE_ROOT, false)

val restartSt = {
val intent = Intent(context, SyncthingService::class.java).apply {
action = SyncthingService.ACTION_RESTART
}
context.startService(intent)
}

val toggleRoot = { enabled: Boolean ->
scope.launch(Dispatchers.IO) {
if (enabled) {
if (RootAccess.isRootAvailableBlocking()) {
withContext(Dispatchers.Main) {
useRoot.value = true
}
// restart after enabling root
restartSt()
} else {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
R.string.toast_root_denied,
Toast.LENGTH_LONG
).show()
}
}
} else {
Util.fixAppDataPermissions(context)
Comment thread
researchxxl marked this conversation as resolved.
withContext(Dispatchers.Main) {
useRoot.value = false
}
restartSt()
}
}
}

SettingsScaffold(
title = stringResource(R.string.category_behaviour),
Expand All @@ -48,5 +96,15 @@ fun SettingsBehaviorScreen() {
state = overwrite,
)
}
item {
SwitchPreference(
title = { Text(stringResource(R.string.use_root_title)) },
summary = { Text(stringResource(R.string.use_root_summary)) },
value = useRoot.value,
onValueChange = {
toggleRoot(it)
},
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ private String getLocalDeviceIDandStoreToPref() throws SyncthingRunnable.Executa
}

private void parseConfig() {
if (!mConfigFile.canRead()) {
if (!mConfigFile.canRead() && !Util.fixAppDataPermissions(mContext)) {
Log.w(TAG, "Failed to open config file '" + mConfigFile + "'");
throw new OpenConfigException();
}
Comment thread
researchxxl marked this conversation as resolved.
Expand Down Expand Up @@ -1261,7 +1261,7 @@ private boolean addSyncthingCameraFolder() {
* Writes updated mConfig back to file.
*/
public void saveChanges() {
if (!mConfigFile.canWrite()) {
if (!mConfigFile.canWrite() && !Util.fixAppDataPermissions(mContext)) {
Log.w(TAG, "Failed to save updated config. Cannot change the owner of the config file.");
return;
}
Expand Down
Loading