From 73775a116df3a71c3030672071712e7ecea58e2a Mon Sep 17 00:00:00 2001 From: Catfriend1 Date: Sat, 22 Sep 2018 18:31:36 +0200 Subject: [PATCH] Improve building wrapper and native binaries on Windows and Linux * Do not ask for root if root is disabled in settings * Show error in UI when libSyncthing.so is missing * build-syncthing - Install Go on demand on windows * build-syncthing - Install Android NDK on demand on windows * Update README.md * Update APK version to 0.14.51.rc3.6 / 4162 --- .gitignore | 3 + README.md | 30 +- app/build.gradle | 4 +- .../activities/FirstStartActivity.java | 24 +- .../activities/WebGuiActivity.java | 17 +- .../service/SyncthingRunnable.java | 73 +++-- .../service/SyncthingService.java | 269 ++++++++++-------- .../syncthingandroid/util/ConfigXml.java | 22 +- app/src/main/res/values/strings.xml | 1 + syncthing/build-syncthing.py | 132 +++++++-- syncthing/build.gradle | 1 + 11 files changed, 377 insertions(+), 199 deletions(-) diff --git a/.gitignore b/.gitignore index 78da3d92..8292ebd7 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ gradle/wrapper/gradlew* # Prebuilt-go syncthing/go syncthing/go.tgz +syncthing/go.zip +syncthing/android-ndk-r16b +syncthing/ndk.zip # External build artifacts ext/ diff --git a/README.md b/README.md index 8105dadd..432d6817 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A wrapper of [Syncthing](https://github.com/syncthing/syncthing) for Android. Head to the "releases" section for builds. Please open an issue under this fork if you need help. Important: Please don't file bugs at the upstream repository "syncthing-android" if you are using this fork. -screenshot 1 screenshot 2 screenshot 3 +screenshot 1 screenshot 2 screenshot 3 # Translations @@ -22,21 +22,33 @@ The project is translated on [Transifex](https://www.transifex.com/projects/p/sy # Building -### Dependencies -- Android SDK (you can skip this if you are using Android Studio) -- Android NDK (`$ANDROID_NDK_HOME` should point at the root directory of your NDK) -- Go (see [here](https://docs.syncthing.net/dev/building.html#prerequisites) for the required version) +### Prerequisites +- Android SDK +`You can skip this if you are using Android Studio.` +- Android NDK r16b +`$ANDROID_NDK_HOME environment variable should point at the root directory of your NDK. If the variable is not set, build-syncthing.py will automatically try to download and setup the NDK.` +- Go 1.9.7 +`Make sure, Go is installed and available on the PATH environment variable. If Go is not found on the PATH environment variable, build-syncthing.py will automatically try to download and setup GO on the PATH.` +- Python 2.7 +`Make sure, Python is installed and available on the PATH environment variable.` ### Build instructions Make sure you clone the project with -`git clone https://github.com/Catfriend1/syncthing-android.git --recursive`. Alternatively, run -`git submodule init && git submodule update` in the project folder. +`git clone https://github.com/Catfriend1/syncthing-android.git --recursive`. +Alternatively, run `git submodule init && git submodule update` in the project folder. A Linux VM, for example running Debian, is recommended to build this. -Build Syncthing using `./gradlew cleanNative buildNative`. Then use `./gradlew assembleDebug` or -Android Studio to build the apk. +Build Syncthing and the Syncthing-Android wrapper using the following commands: +`./gradlew buildNative` +`./gradlew lint assembleDebug` + +You can also use Android Studio to build the apk after you manually ran the `./gradlew buildNative` command in the repository root. + +To clean up all files generated during build, use the following commands: +`./gradlew cleanNative` +`./gradlew clean` # License diff --git a/app/build.gradle b/app/build.gradle index 1ec9d93b..949e6e31 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,8 +35,8 @@ android { applicationId "com.github.catfriend1.syncthingandroid" minSdkVersion 16 targetSdkVersion 26 - versionCode 4161 - versionName "0.14.51.rc3.5" + versionCode 4162 + versionName "0.14.51.rc3.6" testApplicationId 'com.github.catfriend1.syncthingandroid.test' testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner' playAccountConfig = playAccountConfigs.defaultAccountConfig diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/FirstStartActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/FirstStartActivity.java index 4af42d50..c0315577 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/FirstStartActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/FirstStartActivity.java @@ -39,9 +39,11 @@ import android.widget.Toast; import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.SyncthingApp; import com.nutomic.syncthingandroid.service.Constants; +import com.nutomic.syncthingandroid.service.SyncthingRunnable.ExecutableNotFoundException; import com.nutomic.syncthingandroid.util.ConfigXml; import java.lang.ref.WeakReference; + import javax.inject.Inject; public class FirstStartActivity extends Activity { @@ -55,7 +57,7 @@ public class FirstStartActivity extends Activity { public int dotColorActive; public int dotColorInActive; - Slide (int layout, int dotColorActive, int dotColorInActive) { + Slide(int layout, int dotColorActive, int dotColorInActive) { this.layout = layout; this.dotColorActive = dotColorActive; this.dotColorInActive = dotColorInActive; @@ -79,7 +81,8 @@ public class FirstStartActivity extends Activity { private Button mBackButton; private Button mNextButton; - @Inject SharedPreferences mPreferences; + @Inject + SharedPreferences mPreferences; /** * Handles activity behaviour depending on prerequisites. @@ -113,7 +116,7 @@ public class FirstStartActivity extends Activity { // Make notification bar transparent (API level 21+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } // Show first start welcome wizard UI. @@ -138,11 +141,11 @@ public class FirstStartActivity extends Activity { int slideIndex = 0; mSlides = new Slide[ 1 + - (showSlideStoragePermission ? 1 : 0) + - (showSlideIgnoreDozePermission ? 1 : 0) + - (showSlideLocationPermission ? 1 : 0) + - (showSlideKeyGeneration ? 1 : 0) - ]; + (showSlideStoragePermission ? 1 : 0) + + (showSlideIgnoreDozePermission ? 1 : 0) + + (showSlideLocationPermission ? 1 : 0) + + (showSlideKeyGeneration ? 1 : 0) + ]; mSlides[slideIndex++] = new Slide(R.layout.activity_firststart_intro, colorsActive[0], colorsInactive[0]); if (showSlideStoragePermission) { mSlidePosStoragePermission = slideIndex; @@ -365,7 +368,7 @@ public class FirstStartActivity extends Activity { * so that back navigation works as expected. */ if (mPreferences.getBoolean(Constants.PREF_START_INTO_WEB_GUI, false)) { - startActivities(new Intent[] {mainIntent, new Intent(this, WebGuiActivity.class)}); + startActivities(new Intent[]{mainIntent, new Intent(this, WebGuiActivity.class)}); } else { startActivity(mainIntent); } @@ -479,6 +482,9 @@ public class FirstStartActivity extends Activity { } try { configXml = new ConfigXml(firstStartActivity); + } catch (ExecutableNotFoundException e) { + publishProgress(firstStartActivity.getString(R.string.executable_not_found, e.getMessage())); + cancel(true); } catch (ConfigXml.OpenConfigException e) { publishProgress(firstStartActivity.getString(R.string.config_create_failed)); cancel(true); diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java index da9bc10c..c719d2c3 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java @@ -83,7 +83,7 @@ public class WebGuiActivity extends SyncthingActivity SslCertificate sslCert = error.getCertificate(); Field f = sslCert.getClass().getDeclaredField("mX509Certificate"); f.setAccessible(true); - X509Certificate cert = (X509Certificate)f.get(sslCert); + X509Certificate cert = (X509Certificate) f.get(sslCert); if (cert == null) { Log.w(TAG, "X509Certificate reference invalid"); handler.cancel(); @@ -91,8 +91,8 @@ public class WebGuiActivity extends SyncthingActivity } cert.verify(mCaCert.getPublicKey()); handler.proceed(); - } catch (NoSuchFieldException|IllegalAccessException|CertificateException| - NoSuchAlgorithmException|InvalidKeyException|NoSuchProviderException| + } catch (NoSuchFieldException | IllegalAccessException | CertificateException | + NoSuchAlgorithmException | InvalidKeyException | NoSuchProviderException | SignatureException e) { Log.w(TAG, e); handler.cancel(); @@ -106,7 +106,7 @@ public class WebGuiActivity extends SyncthingActivity @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { Uri uri = Uri.parse(url); - if(uri.getHost().equals(getService().getWebGuiUrl().getHost())) { + if (uri.getHost().equals(getService().getWebGuiUrl().getHost())) { return false; } else { startActivity(new Intent(Intent.ACTION_VIEW, uri)); @@ -123,7 +123,6 @@ public class WebGuiActivity extends SyncthingActivity /** * Initialize WebView. - * * Ignore lint javascript warning as js is loaded only from our known, local service. */ @Override @@ -134,7 +133,11 @@ public class WebGuiActivity extends SyncthingActivity setContentView(R.layout.activity_web_gui); mLoadingView = findViewById(R.id.loading); - mConfig = new ConfigXml(this); + try { + mConfig = new ConfigXml(this); + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } loadCaCert(); mWebView = findViewById(R.id.webview); @@ -222,7 +225,7 @@ public class WebGuiActivity extends SyncthingActivity CertificateFactory cf = CertificateFactory.getInstance("X.509"); mCaCert = (X509Certificate) cf.generateCertificate(inStream); - } catch (FileNotFoundException|CertificateException e) { + } catch (FileNotFoundException | CertificateException e) { throw new IllegalArgumentException("Untrusted Certificate", e); } finally { try { diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingRunnable.java b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingRunnable.java index 742fb415..dccd08bf 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingRunnable.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingRunnable.java @@ -56,9 +56,13 @@ public class SyncthingRunnable implements Runnable { private final File mSyncthingBinary; private String[] mCommand; private final File mLogFile; - @Inject SharedPreferences mPreferences; private final boolean mUseRoot; - @Inject NotificationHandler mNotificationHandler; + + @Inject + SharedPreferences mPreferences; + + @Inject + NotificationHandler mNotificationHandler; public enum Command { deviceid, // Output the device ID to the command line. @@ -83,19 +87,19 @@ public class SyncthingRunnable implements Runnable { mUseRoot = mPreferences.getBoolean(Constants.PREF_USE_ROOT, false) && Shell.SU.available(); switch (command) { case deviceid: - mCommand = new String[]{ mSyncthingBinary.getPath(), "-home", mContext.getFilesDir().toString(), "--device-id" }; + mCommand = new String[]{mSyncthingBinary.getPath(), "-home", mContext.getFilesDir().toString(), "--device-id"}; break; case generate: - mCommand = new String[]{ mSyncthingBinary.getPath(), "-generate", mContext.getFilesDir().toString(), "-logflags=0" }; + mCommand = new String[]{mSyncthingBinary.getPath(), "-generate", mContext.getFilesDir().toString(), "-logflags=0"}; break; case main: - mCommand = new String[]{ mSyncthingBinary.getPath(), "-home", mContext.getFilesDir().toString(), "-no-browser", "-logflags=0" }; + mCommand = new String[]{mSyncthingBinary.getPath(), "-home", mContext.getFilesDir().toString(), "-no-browser", "-logflags=0"}; break; case resetdatabase: - mCommand = new String[]{ mSyncthingBinary.getPath(), "-home", mContext.getFilesDir().toString(), "-reset-database", "-logflags=0" }; + mCommand = new String[]{mSyncthingBinary.getPath(), "-home", mContext.getFilesDir().toString(), "-reset-database", "-logflags=0"}; break; case resetdeltas: - mCommand = new String[]{ mSyncthingBinary.getPath(), "-home", mContext.getFilesDir().toString(), "-reset-deltas", "-logflags=0" }; + mCommand = new String[]{mSyncthingBinary.getPath(), "-home", mContext.getFilesDir().toString(), "-reset-deltas", "-logflags=0"}; break; default: throw new InvalidParameterException("Unknown command option"); @@ -104,11 +108,15 @@ public class SyncthingRunnable implements Runnable { @Override public void run() { - run(false); + try { + run(false); + } catch (ExecutableNotFoundException e) { + throw new RuntimeException(e.getMessage()); + } } @SuppressLint("WakelockTimeout") - public String run(boolean returnStdOut) { + public String run(boolean returnStdOut) throws ExecutableNotFoundException { Boolean sendStopToService = false; Boolean restartSyncthingNative = false; int exitCode; @@ -122,7 +130,7 @@ public class SyncthingRunnable implements Runnable { ProcessBuilder pb = new ProcessBuilder("chmod", "500", mSyncthingBinary.getPath()); Process p = pb.start(); p.waitFor(); - } catch (IOException|InterruptedException e) { + } catch (IOException | InterruptedException e) { Log.w(TAG, "Failed to chmod Syncthing", e); } // Loop Syncthing @@ -315,7 +323,11 @@ public class SyncthingRunnable implements Runnable { * Manually run "sysctl fs.inotify" in a root shell terminal to check current limit. */ private void increaseInotifyWatches() { - if (!mUseRoot || !Shell.SU.available()) { + if (!mUseRoot) { + // Settings prohibit using root privileges. Cannot increase inotify limit. + return; + } + if (!Shell.SU.available()) { Log.i(TAG, "increaseInotifyWatches: Root is not available. Cannot increase inotify limit."); return; } @@ -327,7 +339,11 @@ public class SyncthingRunnable implements Runnable { * Look for a running libsyncthing.so process and nice its IO. */ private void niceSyncthing() { - if (!mUseRoot || !Shell.SU.available()) { + if (!mUseRoot) { + // Settings prohibit using root privileges. Cannot nice syncthing. + return; + } + if (!Shell.SU.available()) { Log.i(TAG_NICE, "Root is not available. Cannot nice syncthing."); return; } @@ -343,7 +359,7 @@ public class SyncthingRunnable implements Runnable { // Set best-effort, low priority using ionice. int exitCode = Util.runShellCommand("/system/bin/ionice " + syncthingPID + " be 7\n", true); Log.i(TAG_NICE, "ionice returned " + Integer.toString(exitCode) + - " on " + Constants.FILENAME_SYNCTHING_BINARY); + " on " + Constants.FILENAME_SYNCTHING_BINARY); } } @@ -363,7 +379,7 @@ public class SyncthingRunnable implements Runnable { Log.d(TAG, "Sent kill SIGINT to process " + syncthingPID); } else { Log.w(TAG, "Failed to send kill SIGINT to process " + syncthingPID + - " exit code " + Integer.toString(exitCode)); + " exit code " + Integer.toString(exitCode)); } } @@ -380,9 +396,9 @@ public class SyncthingRunnable implements Runnable { /** * Logs the outputs of a stream to logcat and mNativeLog. * - * @param is The stream to log. + * @param is The stream to log. * @param priority The priority level. - * @param saveLog True if the log should be stored to {@link #mLogFile}. + * @param saveLog True if the log should be stored to {@link #mLogFile}. */ private Thread log(final InputStream is, final int priority, final boolean saveLog) { Thread t = new Thread(() -> { @@ -452,7 +468,7 @@ public class SyncthingRunnable implements Runnable { // Set home directory to data folder for web GUI folder picker. targetEnv.put("HOME", Environment.getExternalStorageDirectory().getAbsolutePath()); targetEnv.put("STTRACE", TextUtils.join(" ", - mPreferences.getStringSet(Constants.PREF_DEBUG_FACILITIES_ENABLED, new HashSet<>()))); + mPreferences.getStringSet(Constants.PREF_DEBUG_FACILITIES_ENABLED, new HashSet<>()))); File externalFilesDir = mContext.getExternalFilesDir(null); if (externalFilesDir != null) targetEnv.put("STGUIASSETS", externalFilesDir.getAbsolutePath() + "/gui"); @@ -482,7 +498,16 @@ public class SyncthingRunnable implements Runnable { return targetEnv; } - private Process setupAndLaunch(HashMap env) throws IOException { + private Process setupAndLaunch(HashMap env) throws IOException, ExecutableNotFoundException { + // Check if "libSyncthing.so" exists. + if (mCommand.length > 0) { + File libSyncthing = new File(mCommand[0]); + if (!libSyncthing.exists()) { + Log.e(TAG, "CRITICAL - Syncthing core binary is missing in APK package location " + mCommand[0]); + throw new ExecutableNotFoundException(mCommand[0]); + } + } + if (mUseRoot) { ProcessBuilder pb = new ProcessBuilder("su"); Process process = pb.start(); @@ -508,4 +533,16 @@ public class SyncthingRunnable implements Runnable { return pb.start(); } } + + public class ExecutableNotFoundException extends Exception { + + public ExecutableNotFoundException(String message) { + super(message); + } + + public ExecutableNotFoundException(String message, Throwable throwable) { + super(message, throwable); + } + + } } diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java index e21f8e79..e82840c9 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java @@ -120,18 +120,27 @@ public class SyncthingService extends Service { * Indicates the current state of SyncthingService and of Syncthing itself. */ public enum State { - /** Service is initializing, Syncthing was not started yet. */ + /** + * Service is initializing, Syncthing was not started yet. + */ INIT, - /** Syncthing binary is starting. */ + /** + * Syncthing binary is starting. + */ STARTING, - /** Syncthing binary is running, + /** + * Syncthing binary is running, * Rest API is available, * RestApi class read the config and is fully initialized. */ ACTIVE, - /** Syncthing binary is shutting down. */ + /** + * Syncthing binary is shutting down. + */ DISABLED, - /** There is some problem that prevents Syncthing from running. */ + /** + * There is some problem that prevents Syncthing from running. + */ ERROR, } @@ -141,13 +150,7 @@ public class SyncthingService extends Service { * {@link onStartCommand}. */ private State mCurrentState = State.DISABLED; - private ConfigXml mConfig; - private @Nullable PollWebGuiAvailableTask mPollWebGuiAvailableTask = null; - private @Nullable RestApi mApi = null; - private @Nullable EventProcessor mEventProcessor = null; - private @Nullable RunConditionMonitor mRunConditionMonitor = null; - private @Nullable SyncthingRunnable mSyncthingRunnable = null; private StartupTask mStartupTask = null; private Thread mSyncthingRunnableThread = null; private Handler mHandler; @@ -155,8 +158,26 @@ public class SyncthingService extends Service { private final HashSet mOnServiceStateChangeListeners = new HashSet<>(); private final SyncthingServiceBinder mBinder = new SyncthingServiceBinder(this); - @Inject NotificationHandler mNotificationHandler; - @Inject SharedPreferences mPreferences; + private @Nullable + PollWebGuiAvailableTask mPollWebGuiAvailableTask = null; + + private @Nullable + RestApi mApi = null; + + private @Nullable + EventProcessor mEventProcessor = null; + + private @Nullable + RunConditionMonitor mRunConditionMonitor = null; + + private @Nullable + SyncthingRunnable mSyncthingRunnable = null; + + @Inject + NotificationHandler mNotificationHandler; + + @Inject + SharedPreferences mPreferences; /** * Object that must be locked upon accessing mCurrentState @@ -196,8 +217,8 @@ public class SyncthingService extends Service { * We need to recheck if we still have the storage permission. */ mStoragePermissionGranted = (ContextCompat.checkSelfPermission(this, - Manifest.permission.WRITE_EXTERNAL_STORAGE) == - PackageManager.PERMISSION_GRANTED); + Manifest.permission.WRITE_EXTERNAL_STORAGE) == + PackageManager.PERMISSION_GRANTED); } /** @@ -223,7 +244,7 @@ public class SyncthingService extends Service { * See {@link mLastDeterminedShouldRun} defaulting to "false". */ if (mCurrentState == State.DISABLED) { - synchronized(mStateLock) { + synchronized (mStateLock) { onServiceStateChange(mCurrentState); } } @@ -245,7 +266,8 @@ public class SyncthingService extends Service { if (ACTION_RESTART.equals(intent.getAction()) && mCurrentState == State.ACTIVE) { shutdown(State.INIT, () -> launchStartupTask(SyncthingRunnable.Command.main)); } else if (ACTION_STOP.equals(intent.getAction()) && mCurrentState == State.ACTIVE) { - shutdown(State.DISABLED, () -> {}); + shutdown(State.DISABLED, () -> { + }); } else if (ACTION_RESET_DATABASE.equals(intent.getAction())) { Log.i(TAG, "Invoking reset of database"); shutdown(State.INIT, () -> { @@ -305,7 +327,8 @@ public class SyncthingService extends Service { return; } Log.v(TAG, "Stopping syncthing"); - shutdown(State.DISABLED, () -> {}); + shutdown(State.DISABLED, () -> { + }); } } } @@ -313,9 +336,9 @@ public class SyncthingService extends Service { /** * Prepares to launch the syncthing binary. */ - private void launchStartupTask (SyncthingRunnable.Command srCommand) { + private void launchStartupTask(SyncthingRunnable.Command srCommand) { Log.v(TAG, "Starting syncthing"); - synchronized(mStateLock) { + synchronized (mStateLock) { if (mCurrentState != State.DISABLED && mCurrentState != State.INIT) { Log.e(TAG, "launchStartupTask: Wrong state " + mCurrentState + " detected. Cancelling."); return; @@ -336,90 +359,96 @@ public class SyncthingService extends Service { * Sets up the initial configuration, and updates the config when coming from an old * version. */ - private static class StartupTask extends AsyncTask { - private WeakReference refSyncthingService; - private SyncthingRunnable.Command srCommand; + private static class StartupTask extends AsyncTask { + private WeakReference refSyncthingService; + private SyncthingRunnable.Command srCommand; - StartupTask(SyncthingService context, SyncthingRunnable.Command srCommand) { - refSyncthingService = new WeakReference<>(context); - this.srCommand = srCommand; - } + StartupTask(SyncthingService context, SyncthingRunnable.Command srCommand) { + refSyncthingService = new WeakReference<>(context); + this.srCommand = srCommand; + } - @Override - protected Void doInBackground(Void... voids) { - SyncthingService syncthingService = refSyncthingService.get(); - if (syncthingService == null) { - cancel(true); - return null; - } - try { - syncthingService.mConfig = new ConfigXml(syncthingService); - syncthingService.mConfig.updateIfNeeded(); - } catch (ConfigXml.OpenConfigException e) { - syncthingService.mNotificationHandler.showCrashedNotification(R.string.config_read_failed, "ConfigXml.OpenConfigException"); - synchronized (syncthingService.mStateLock) { - syncthingService.onServiceStateChange(State.ERROR); - } - cancel(true); - } - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - // Get a reference to the service if it is still there. - SyncthingService syncthingService = refSyncthingService.get(); - if (syncthingService != null) { - syncthingService.onStartupTaskCompleteListener(srCommand); - } - } - } - - /** - * Callback on {@link StartupTask#onPostExecute}. - */ - private void onStartupTaskCompleteListener(SyncthingRunnable.Command srCommand) { - if (mApi == null) { - mApi = new RestApi(this, mConfig.getWebGuiUrl(), mConfig.getApiKey(), - this::onApiAvailable, () -> onServiceStateChange(mCurrentState)); - Log.i(TAG, "Web GUI will be available at " + mConfig.getWebGuiUrl()); - } - - // Check mSyncthingRunnable lifecycle and create singleton. - if (mSyncthingRunnable != null || mSyncthingRunnableThread != null) { - Log.e(TAG, "onStartupTaskCompleteListener: Syncthing binary lifecycle violated"); - return; - } - mSyncthingRunnable = new SyncthingRunnable(this, srCommand); - - /** - * Check if an old syncthing instance is still running. - * This happens after an in-place app upgrade. If so, end it. - */ - mSyncthingRunnable.killSyncthing(); - - // Start the syncthing binary in a separate thread. - mSyncthingRunnableThread = new Thread(mSyncthingRunnable); - mSyncthingRunnableThread.start(); - - /** - * Wait for the web-gui of the native syncthing binary to come online. - * - * In case the binary is to be stopped, also be aware that another thread could request - * to stop the binary in the time while waiting for the GUI to become active. See the comment - * for {@link SyncthingService#onDestroy} for details. - */ - if (mPollWebGuiAvailableTask == null) { - mPollWebGuiAvailableTask = new PollWebGuiAvailableTask( - this, getWebGuiUrl(), mConfig.getApiKey(), result -> { - Log.i(TAG, "Web GUI has come online at " + mConfig.getWebGuiUrl()); - if (mApi != null) { - mApi.readConfigFromRestApi(); - } + @Override + protected Void doInBackground(Void... voids) { + SyncthingService syncthingService = refSyncthingService.get(); + if (syncthingService == null) { + cancel(true); + return null; + } + try { + syncthingService.mConfig = new ConfigXml(syncthingService); + syncthingService.mConfig.updateIfNeeded(); + } catch (SyncthingRunnable.ExecutableNotFoundException e) { + syncthingService.mNotificationHandler.showCrashedNotification(R.string.config_read_failed, "SycnthingRunnable.ExecutableNotFoundException"); + synchronized (syncthingService.mStateLock) { + syncthingService.onServiceStateChange(State.ERROR); } - ); - } - } + cancel(true); + } catch (ConfigXml.OpenConfigException e) { + syncthingService.mNotificationHandler.showCrashedNotification(R.string.config_read_failed, "ConfigXml.OpenConfigException"); + synchronized (syncthingService.mStateLock) { + syncthingService.onServiceStateChange(State.ERROR); + } + cancel(true); + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + // Get a reference to the service if it is still there. + SyncthingService syncthingService = refSyncthingService.get(); + if (syncthingService != null) { + syncthingService.onStartupTaskCompleteListener(srCommand); + } + } + } + + /** + * Callback on {@link StartupTask#onPostExecute}. + */ + private void onStartupTaskCompleteListener(SyncthingRunnable.Command srCommand) { + if (mApi == null) { + mApi = new RestApi(this, mConfig.getWebGuiUrl(), mConfig.getApiKey(), + this::onApiAvailable, () -> onServiceStateChange(mCurrentState)); + Log.i(TAG, "Web GUI will be available at " + mConfig.getWebGuiUrl()); + } + + // Check mSyncthingRunnable lifecycle and create singleton. + if (mSyncthingRunnable != null || mSyncthingRunnableThread != null) { + Log.e(TAG, "onStartupTaskCompleteListener: Syncthing binary lifecycle violated"); + return; + } + mSyncthingRunnable = new SyncthingRunnable(this, srCommand); + + /** + * Check if an old syncthing instance is still running. + * This happens after an in-place app upgrade. If so, end it. + */ + mSyncthingRunnable.killSyncthing(); + + // Start the syncthing binary in a separate thread. + mSyncthingRunnableThread = new Thread(mSyncthingRunnable); + mSyncthingRunnableThread.start(); + + /** + * Wait for the web-gui of the native syncthing binary to come online. + * + * In case the binary is to be stopped, also be aware that another thread could request + * to stop the binary in the time while waiting for the GUI to become active. See the comment + * for {@link SyncthingService#onDestroy} for details. + */ + if (mPollWebGuiAvailableTask == null) { + mPollWebGuiAvailableTask = new PollWebGuiAvailableTask( + this, getWebGuiUrl(), mConfig.getApiKey(), result -> { + Log.i(TAG, "Web GUI has come online at " + mConfig.getWebGuiUrl()); + if (mApi != null) { + mApi.readConfigFromRestApi(); + } + } + ); + } + } /** * Called when {@link RestApi#checkReadConfigFromRestApiCompleted} detects @@ -483,21 +512,22 @@ public class SyncthingService extends Service { mDestroyScheduled = true; } else { Log.i(TAG, "Shutting down syncthing binary immediately"); - shutdown(State.DISABLED, () -> {}); + shutdown(State.DISABLED, () -> { + }); } } } else { // If the storage permission got revoked, we did not start the binary and // are in State.INIT requiring an immediate shutdown of this service class. Log.i(TAG, "Shutting down syncthing binary due to missing storage permission."); - shutdown(State.DISABLED, () -> {}); + shutdown(State.DISABLED, () -> { + }); } super.onDestroy(); } /** * Stop Syncthing and all helpers like event processor and api handler. - * * Sets {@link #mCurrentState} to newState, and calls onKilledListener once Syncthing is killed. */ private void shutdown(State newState, OnSyncthingKilled onKilledListener) { @@ -510,7 +540,7 @@ public class SyncthingService extends Service { } Log.i(TAG, "Shutting down"); - synchronized(mStateLock) { + synchronized (mStateLock) { onServiceStateChange(newState); } @@ -550,7 +580,8 @@ public class SyncthingService extends Service { onKilledListener.onKilled(); } - public @Nullable RestApi getApi() { + public @Nullable + RestApi getApi() { return mApi; } @@ -568,7 +599,6 @@ public class SyncthingService extends Service { /** * Register a listener for the syncthing API state changing. - * * The listener is called immediately with the current state, and again whenever the state * changes. The call is always from the GUI thread. * @@ -658,12 +688,12 @@ public class SyncthingService extends Service { file = new File(Constants.EXPORT_PATH, Constants.SHARED_PREFS_EXPORT_FILE); fileOutputStream = new FileOutputStream(file); if (!file.exists()) { - file.createNewFile(); - } + file.createNewFile(); + } objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(mPreferences.getAll()); objectOutputStream.flush(); - fileOutputStream.flush(); + fileOutputStream.flush(); } catch (IOException e) { Log.e(TAG, "exportConfig: Failed to export SharedPreferences #1", e); failSuccess = false; @@ -672,12 +702,12 @@ public class SyncthingService extends Service { if (objectOutputStream != null) { objectOutputStream.close(); } - if (fileOutputStream != null) { - fileOutputStream.close(); - } - } catch (IOException e) { - Log.e(TAG, "exportConfig: Failed to export SharedPreferences #2", e); - } + if (fileOutputStream != null) { + fileOutputStream.close(); + } + } catch (IOException e) { + Log.e(TAG, "exportConfig: Failed to export SharedPreferences #2", e); + } } return failSuccess; } @@ -692,7 +722,8 @@ public class SyncthingService extends Service { Log.v(TAG, "importConfig BEGIN"); // Shutdown synchronously. - shutdown(State.DISABLED, () -> {}); + shutdown(State.DISABLED, () -> { + }); // Import config, privateKey and/or publicKey. try { @@ -736,11 +767,13 @@ public class SyncthingService extends Service { // Preferences that are no longer used and left-overs from previous versions of the app. case "first_start": case "notify_crashes": + Log.v(TAG, "importConfig: Ignoring deprecated pref \"" + prefKey + "\"."); + break; // Cached information which is not available on SettingsActivity. case Constants.PREF_DEBUG_FACILITIES_AVAILABLE: case Constants.PREF_EVENT_PROCESSOR_LAST_SYNC_ID: case Constants.PREF_LAST_BINARY_VERSION: - Log.v(TAG, "importConfig: Ignoring pref \"" + prefKey + "\"."); + Log.v(TAG, "importConfig: Ignoring cache pref \"" + prefKey + "\"."); break; default: Log.v(TAG, "importConfig: Adding pref \"" + prefKey + "\" to commit ..."); diff --git a/app/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java b/app/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java index 9b934f69..f3eb9353 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java @@ -41,7 +41,6 @@ import javax.xml.transform.stream.StreamResult; /** * Provides direct access to the config.xml file in the file system. - * * This class should only be used if the syncthing API is not available (usually during startup). */ public class ConfigXml { @@ -53,19 +52,21 @@ public class ConfigXml { private static final int FOLDER_ID_APPENDIX_LENGTH = 4; private final Context mContext; - @Inject SharedPreferences mPreferences; + + @Inject + SharedPreferences mPreferences; private final File mConfigFile; private Document mConfig; - public ConfigXml(Context context) throws OpenConfigException { + public ConfigXml(Context context) throws OpenConfigException, SyncthingRunnable.ExecutableNotFoundException { mContext = context; mConfigFile = Constants.getConfigFile(mContext); boolean isFirstStart = !mConfigFile.exists(); if (isFirstStart) { Log.i(TAG, "App started for the first time. Generating keys and config."); - new SyncthingRunnable(context, SyncthingRunnable.Command.generate).run(); + new SyncthingRunnable(context, SyncthingRunnable.Command.generate).run(true); } readConfig(); @@ -122,7 +123,6 @@ public class ConfigXml { /** * Updates the config file. - * * Sets ignorePerms flag to true on every folder, force enables TLS, sets the * username/password, and disables weak hash checking. */ @@ -210,12 +210,11 @@ public class ConfigXml { /** * Updates syncthing options to a version specific target setting in the config file. - * * Used for one-time config migration from a lower syncthing version to the current version. * Enables filesystem watcher. * Returns if changes to the config have been made. */ - private boolean migrateSyncthingOptions () { + private boolean migrateSyncthingOptions() { /* Read existing config version */ int iConfigVersion = Integer.parseInt(mConfig.getDocumentElement().getAttribute("version")); int iOldConfigVersion = iConfigVersion; @@ -238,10 +237,10 @@ public class ConfigXml { } /** - * Set config version to 28 after manual config migration - * This prevents "unackedNotificationID" getting populated - * with the fsWatcher GUI notification. - */ + * Set config version to 28 after manual config migration + * This prevents "unackedNotificationID" getting populated + * with the fsWatcher GUI notification. + */ iConfigVersion = 28; } @@ -273,7 +272,6 @@ public class ConfigXml { /** * Set device model name as device name for Syncthing. - * * We need to iterate through XML nodes manually, as mConfig.getDocumentElement() will also * return nested elements inside folder element. We have to check that we only rename the * device corresponding to the local device ID. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0aa4055e..916567f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -652,6 +652,7 @@ Please report any problems you encounter via Github. Syncthing was terminated + Core executable \"%s\" is missing. Check build and logcat output. Failed to create configuration. Check logcat output. Failed to read configuration. Consider backing up data from your sync folders, then clear this app\'s data from Android settings and launch it again. diff --git a/syncthing/build-syncthing.py b/syncthing/build-syncthing.py index 5f90aeb5..d162868d 100644 --- a/syncthing/build-syncthing.py +++ b/syncthing/build-syncthing.py @@ -44,17 +44,13 @@ def get_min_sdk(project_dir): fail('Failed to find minSdkVersion') - -def get_ndk_home(): - if not os.environ.get('ANDROID_NDK_HOME', ''): - fail('Error: ANDROID_NDK_HOME environment variable not defined') - return os.environ['ANDROID_NDK_HOME'] - def which(program): import os def is_exe(fpath): return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + if (sys.platform == 'win32'): + program += ".exe" fpath, fname = os.path.split(program) if fpath: if is_exe(program): @@ -67,26 +63,40 @@ def which(program): return None +def change_permissions_recursive(path, mode): + import os + for root, dirs, files in os.walk(path, topdown=False): + for dir in [os.path.join(root,d) for d in dirs]: + os.chmod(dir, mode) + for file in [os.path.join(root, f) for f in files]: + os.chmod(file, mode) + def install_go(): import os import tarfile + import zipfile import urllib import hashlib # Consts. pwd_path = os.path.dirname(os.path.realpath(__file__)) - url = 'https://dl.google.com/go/go1.9.7.linux-amd64.tar.gz' - expected_shasum = '88573008f4f6233b81f81d8ccf92234b4f67238df0f0ab173d75a302a1f3d6ee' + if sys.platform == 'win32': + url = 'https://dl.google.com/go/go1.9.7.windows-amd64.zip' + expected_shasum = '8db4b21916a3bc79f48d0611202ee5814c82f671b36d5d2efcb446879456cd28' + tar_gz_fullfn = pwd_path + os.path.sep + 'go.zip'; + else: + url = 'https://dl.google.com/go/go1.9.7.linux-amd64.tar.gz' + expected_shasum = '88573008f4f6233b81f81d8ccf92234b4f67238df0f0ab173d75a302a1f3d6ee' + tar_gz_fullfn = pwd_path + os.path.sep + 'go.tgz'; # Download prebuilt-go. url_base_name = os.path.basename(url) - tar_gz_fullfn = pwd_path + os.path.sep + 'go.tgz'; if not os.path.isfile(tar_gz_fullfn): - print('Downloading prebuilt-go tar to:', tar_gz_fullfn) + print('Downloading prebuilt-go to:', tar_gz_fullfn) tar_gz_fullfn = urllib.urlretrieve(url, tar_gz_fullfn)[0] - print('Downloaded prebuilt-go tar to:', tar_gz_fullfn) + print('Downloaded prebuilt-go to:', tar_gz_fullfn) - # Verfiy SHA-1 checksum of downloaded files. + # Verfiy SHA-256 checksum of downloaded files. with open(tar_gz_fullfn, 'rb') as f: contents = f.read() found_shasum = hashlib.sha256(contents).hexdigest() @@ -96,11 +106,18 @@ def install_go(): print("[ok] Checksum of", tar_gz_fullfn, "matches expected value.") # Proceed with extraction of the prebuilt go. - # This will go to a subfolder "go" in the current path. - print("Extracting prebuilt-go ...") - file_name, file_extension = os.path.splitext(url_base_name) - tar = tarfile.open(tar_gz_fullfn) - tar.extractall(pwd_path) + if not os.path.isfile(pwd_path + os.path.sep + 'go' + os.path.sep + 'LICENSE'): + print("Extracting prebuilt-go ...") + # This will go to a subfolder "go" in the current path. + file_name, file_extension = os.path.splitext(url_base_name) + if sys.platform == 'win32': + zip = zipfile.ZipFile(tar_gz_fullfn, 'r') + zip.extractall(pwd_path) + zip.close() + else: + tar = tarfile.open(tar_gz_fullfn) + tar.extractall(pwd_path) + tar.close() # Add (...).tar/go/bin" to the PATH. go_bin_path = pwd_path + os.path.sep + 'go' + os.path.sep + 'bin' @@ -108,6 +125,65 @@ def install_go(): os.environ["PATH"] += os.pathsep + go_bin_path + + +def install_ndk(): + import os + import zipfile + import urllib + import hashlib + + # Consts. + pwd_path = os.path.dirname(os.path.realpath(__file__)) + if sys.platform == 'win32': + url = 'https://dl.google.com/android/repository/android-ndk-r16b-windows-x86_64.zip' + expected_shasum = 'f3f1909ed1052e98dda2c79d11c22f3da28daf25' + + else: + url = 'https://dl.google.com/android/repository/android-ndk-r16b-linux-x86_64.zip' + expected_shasum = '42aa43aae89a50d1c66c3f9fdecd676936da6128' + + zip_fullfn = pwd_path + os.path.sep + 'ndk.zip'; + # Download NDK. + url_base_name = os.path.basename(url) + if not os.path.isfile(zip_fullfn): + print('Downloading NDK to:', zip_fullfn) + zip_fullfn = urllib.urlretrieve(url, zip_fullfn)[0] + print('Downloaded NDK to:', zip_fullfn) + + # Verfiy SHA-1 checksum of downloaded files. + with open(zip_fullfn, 'rb') as f: + contents = f.read() + found_shasum = hashlib.sha1(contents).hexdigest() + print("SHA-1:", zip_fullfn, "%s" % found_shasum) + if found_shasum != expected_shasum: + fail('Error: SHA-1 checksum', found_shasum, 'of downloaded file does not match expected checksum', expected_shasum) + print("[ok] Checksum of", zip_fullfn, "matches expected value.") + + # Proceed with extraction of the NDK if necessary. + ndk_home_path = pwd_path + os.path.sep + 'android-ndk-r16b' + if not os.path.isfile(ndk_home_path + os.path.sep + "sysroot" + os.path.sep + "NOTICE"): + print("Extracting NDK ...") + # This will go to a subfolder "android-ndk-r16b" in the current path. + file_name, file_extension = os.path.splitext(url_base_name) + zip = zipfile.ZipFile(zip_fullfn, 'r') + zip.extractall(pwd_path) + zip.close() + + # Linux only - Set executable permission on files. + if platform.system() == 'Linux': + print("Setting permissions on NDK executables ...") + change_permissions_recursive(ndk_home_path, 0o755); + + # Add "ANDROID_NDK_HOME" environment variable. + print('Adding ANDROID_NDK_HOME=\'' + ndk_home_path + '\'') + os.environ["ANDROID_NDK_HOME"] = ndk_home_path + + + +# +# BUILD SCRIPT MAIN. +# if platform.system() not in SUPPORTED_PYTHON_PLATFORMS: fail('Unsupported python platform %s. Supported platforms: %s', platform.system(), ', '.join(SUPPORTED_PYTHON_PLATFORMS)) @@ -125,12 +201,20 @@ go_bin = which("go"); if not go_bin: print('Warning: go is not available on the PATH.') install_go(); + # Retry: Check if go is available. + go_bin = which("go"); + if not go_bin: + fail('Error: go is not available on the PATH.') +print ('go_bin=\'' + go_bin + '\'') -# Retry: Check if go is available. -go_bin = which("go"); -if not go_bin: - fail('Error: go is not available on the PATH.') -print ('go_bin [', go_bin, ']') +# Check if ANDROID_NDK_HOME variable is set. +if not os.environ.get('ANDROID_NDK_HOME', ''): + print('Warning: ANDROID_NDK_HOME environment variable not defined.') + install_ndk(); + # Retry: Check if ANDROID_NDK_HOME variable is set. + if not os.environ.get('ANDROID_NDK_HOME', ''): + fail('Error: ANDROID_NDK_HOME environment variable not defined') +print ('ANDROID_NDK_HOME=\'' + os.environ.get('ANDROID_NDK_HOME', '') + '\'') # Make sure all tags are available for git describe # https://github.com/syncthing/syncthing-android/issues/872 @@ -148,7 +232,7 @@ for target in BUILD_TARGETS: if os.environ.get('SYNCTHING_ANDROID_PREBUILT', ''): # The environment variable indicates the SDK and stdlib was prebuilt, set a custom paths. - standalone_ndk_dir = get_ndk_home() + os.path.sep + 'standalone-ndk' + os.path.sep + 'android-' + target_min_sdk + '-' + target['goarch'] + standalone_ndk_dir = os.environ['ANDROID_NDK_HOME'] + os.path.sep + 'standalone-ndk' + os.path.sep + 'android-' + target_min_sdk + '-' + target['goarch'] pkg_argument = [] else: # Build standalone NDK toolchain if it doesn't exist. @@ -160,7 +244,7 @@ for target in BUILD_TARGETS: print('Building standalone NDK for', target['arch'], 'API level', target_min_sdk, 'to', standalone_ndk_dir) subprocess.check_call([ sys.executable, - os.path.join(get_ndk_home(), 'build', 'tools', 'make_standalone_toolchain.py'), + os.path.join(os.environ['ANDROID_NDK_HOME'], 'build', 'tools', 'make_standalone_toolchain.py'), '--arch', target['arch'], '--api', diff --git a/syncthing/build.gradle b/syncthing/build.gradle index 437af669..54a3375d 100644 --- a/syncthing/build.gradle +++ b/syncthing/build.gradle @@ -10,6 +10,7 @@ task buildNative(type: Exec) { */ task cleanNative(type: Delete) { delete "$projectDir/../app/src/main/jniLibs/" + delete "android-ndk-r16b" delete "gobuild" delete "go" }