diff --git a/README.md b/README.md index 7538608a..206ee350 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ Set the `ANDROID_NDK` environment variable to the Android NDK folder (e.g. `expo Build Go and Syncthing using `./make-all.bash`. Use `./gradlew assembleDebug` in the project directory to compile the APK. +To prepare a new release, execute `./prepare-release.bash`, and follow the instructions. + To check for updated gradle dependencies, run `gradle dependencyUpdates`. Additionally, the git submodule in `ext/syncthing/src/github.com/syncthing/syncthing` may need to be updated. diff --git a/build.gradle b/build.gradle index e9f66baa..542280a0 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,8 @@ dependencies { compile 'com.google.zxing:android-integration:3.2.1' compile 'com.google.code.gson:gson:2.7' compile 'org.mindrot:jbcrypt:0.3m' + testCompile 'junit:junit:4.12' + testCompile 'org.robolectric:robolectric:3.1.2' androidTestCompile 'com.squareup.okhttp:mockwebserver:2.4.0' } diff --git a/prepare-release.bash b/prepare-release.bash new file mode 100755 index 00000000..e18021f8 --- /dev/null +++ b/prepare-release.bash @@ -0,0 +1,79 @@ +#!/bin/bash + +set -e + +NEW_VERSION_NAME=$1 +OLD_VERSION_NAME=$(grep "versionName" "build.gradle" | awk '{print $2}') +if [[ -z $NEW_VERSION_NAME ]] +then + echo "New version name is empty. Please set a new version. Current version: $OLD_VERSION_NAME" + exit +fi + +echo " + +Checking for Syncthing Update +----------------------------- +" +PROJECT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "ext/syncthing/src/github.com/syncthing/syncthing/" +git fetch +CURRENT_TAG=$(git describe) +LATEST_TAG=$(git describe $(git rev-list --tags --max-count=1)) +if [ $CURRENT_TAG != $LATEST_TAG ] +then + git checkout -f $LATEST_TAG + cd $PROJECT_DIR + git add "ext/syncthing/src/github.com/syncthing/syncthing" + git commit -m "Updated Syncthing to $LATEST_TAG" + ./gradlew cleanNative buildNative +fi +cd $PROJECT_DIR + + +echo " + +Updating Translations +----------------------------- +" +tx push -s +tx pull -a +./gradlew deleteUnsupportedPlayTranslations publishListingFatRelease +git add -A "src/fat/play/" +git add -A "src/main/res/values-*/strings.xml" +if ! git diff --cached --exit-code; +then + git commit -m "Imported translations" +fi + + +echo " + +Running Tests +----------------------------- +" +./gradlew lint +./gradlew connectedFatDebugAndroidTest +./gradlew testFatDebugUnitTest + +echo " + +Updating Version +----------------------------- +" +OLD_VERSION_CODE=$(grep "versionCode" "build.gradle" -m 1 | awk '{print $2}') +NEW_VERSION_CODE=$(($OLD_VERSION_CODE + 1)) +sed -i "s/versionCode $OLD_VERSION_CODE/versionCode $NEW_VERSION_CODE/" build.gradle + +OLD_VERSION_NAME=$(grep "versionName" "build.gradle" | awk '{print $2}') +sed -i "s/$OLD_VERSION_NAME/\"$1\"/" build.gradle +git add "build.gradle" +git commit -m "Bumped version to $NEW_VERSION_NAME" + +echo " +Update ready. +1. Run \`git push --follow-tags\` +2. Enter release notes at https://github.com/syncthing/syncthing-android/releases +3. Enter release notes at https://android.syncthing.net/job/Syncthing-Android-Release/configure +4. Start build at https://android.syncthing.net/job/Syncthing-Android-Release +" diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/MockRestApi.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/MockRestApi.java index d6d04710..d21d5227 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/MockRestApi.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/MockRestApi.java @@ -11,9 +11,8 @@ import java.util.List; public class MockRestApi extends RestApi { public MockRestApi(Context context, String url, String apiKey, - String guiUser, String guiPassword, OnApiAvailableListener listener) { - super(context, url, apiKey, guiUser, guiPassword, listener, null); + super(context, url, apiKey, listener, null); } @Override diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/MockSyncthingService.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/MockSyncthingService.java index 48140553..da4fa1cc 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/MockSyncthingService.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/MockSyncthingService.java @@ -49,7 +49,7 @@ public class MockSyncthingService extends SyncthingService { @Override public RestApi getApi() { - return new MockRestApi(this, null, null, null, null, null); + return new MockRestApi(this, null, null, null); } @Override diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java index c67d359c..b9d95066 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java @@ -26,7 +26,6 @@ public class RestApiTest extends AndroidTestCase { new SyncthingRunnable(new MockContext(getContext()), SyncthingRunnable.Command.main); ConfigXml config = new ConfigXml(new MockContext(getContext())); - config.changeDefaultFolder(); String httpsCertPath = getContext().getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE; @@ -39,7 +38,6 @@ public class RestApiTest extends AndroidTestCase { } }.execute(config.getWebGuiUrl()); mApi = new RestApi(getContext(), config.getWebGuiUrl(), config.getApiKey(), - null, null, new RestApi.OnApiAvailableListener() { @Override public void onApiAvailable() { diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/SyncthingServiceTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/SyncthingServiceTest.java deleted file mode 100644 index 33d6c963..00000000 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/SyncthingServiceTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.nutomic.syncthingandroid.test.syncthing; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.test.ServiceTestCase; - -import com.nutomic.syncthingandroid.syncthing.SyncthingService; -import com.nutomic.syncthingandroid.syncthing.SyncthingServiceBinder; -import com.nutomic.syncthingandroid.test.MockContext; -import com.nutomic.syncthingandroid.test.Util; -import com.nutomic.syncthingandroid.util.ConfigXml; - -import java.io.File; -import java.io.IOException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * These tests assume that syncthing keys have already been generated. If not, tests may fail - * because startup takes too long. - */ -public class SyncthingServiceTest extends ServiceTestCase { - - private Context mContext; - - public SyncthingServiceTest() { - super(SyncthingService.class); - } - - @Override - protected void setUp() throws Exception { - super.setUp(); - mContext = new MockContext(getContext()); - } - - @Override - protected void tearDown() throws Exception { - Util.deleteRecursive(mContext.getFilesDir()); - PreferenceManager.getDefaultSharedPreferences(getContext()).edit().clear().commit(); - super.tearDown(); - } - - public void testFirstStart() { - setContext(mContext); - startService(new Intent(mContext, SyncthingService.class)); - assertTrue(getService().isFirstStart()); - } - - public void testNotFirstStart() throws IOException { - setContext(mContext); - startService(new Intent(mContext, SyncthingService.class)); - new File(mContext.getFilesDir(), SyncthingService.PUBLIC_KEY_FILE).createNewFile(); - assertFalse(getService().isFirstStart()); - } - - public void testBindService() throws InterruptedException { - SyncthingServiceBinder binder = (SyncthingServiceBinder) - bindService(new Intent(getContext(), SyncthingService.class)); - SyncthingService service = binder.getService(); - final CountDownLatch latch = new CountDownLatch(2); - getService().registerOnWebGuiAvailableListener(new SyncthingService.OnWebGuiAvailableListener() { - @Override - public void onWebGuiAvailable() { - latch.countDown(); - } - }); - getService().registerOnApiChangeListener(new SyncthingService.OnApiChangeListener() { - @Override - public void onApiChange(SyncthingService.State currentState) { - latch.countDown(); - } - }); - latch.await(1, TimeUnit.SECONDS); - assertNotNull(service); - } - - public void testImportExportConfig() { - setContext(mContext); - SyncthingServiceBinder binder = (SyncthingServiceBinder) - bindService(new Intent(getContext(), SyncthingService.class)); - SyncthingService service = binder.getService(); - File config = new File(mContext.getFilesDir(), ConfigXml.CONFIG_FILE); - File privateKey = new File(mContext.getFilesDir(), SyncthingService.PRIVATE_KEY_FILE); - File publicKey = new File(mContext.getFilesDir(), SyncthingService.PUBLIC_KEY_FILE); - - try { - config.createNewFile(); - privateKey.createNewFile(); - publicKey.createNewFile(); - } catch (IOException e) { - fail(); - } - - service.exportConfig(); - - config.delete(); - privateKey.delete(); - publicKey.delete(); - - service.importConfig(); - assertTrue(config.exists()); - assertTrue(privateKey.exists()); - assertTrue(publicKey.exists()); - } - - public void testPassword() throws InterruptedException { - startService(new Intent(getContext(), SyncthingService.class)); - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); - assertNotNull(sp.getString("gui_user", null)); - assertEquals(20, sp.getString("gui_password", null).length()); - } - }, 5000); - } - -} diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ConfigXmlTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ConfigXmlTest.java index 21eb6491..0bbb6ab2 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ConfigXmlTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ConfigXmlTest.java @@ -38,17 +38,4 @@ public class ConfigXmlTest extends AndroidTestCase { assertTrue(mConfig.getWebGuiUrl().startsWith("https://127.0.0.1:")); } - /** - * Just make sure the file is actually changed. - * - * This is not ideal, but way less complicated than starting up syncthing and accessing the API. - */ - public void testCreateCameraFolder() { - long oldTime = ConfigXml.getConfigFile(mContext).lastModified(); - long oldSize = ConfigXml.getConfigFile(mContext).length(); - mConfig.changeDefaultFolder(); - assertNotSame(oldTime, ConfigXml.getConfigFile(mContext).lastModified()); - assertNotSame(oldSize, ConfigXml.getConfigFile(mContext).lastModified()); - } - } diff --git a/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java b/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java index 963a29b7..4ef90dce 100644 --- a/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java +++ b/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java @@ -59,8 +59,6 @@ public class ConfigXml { */ public static final String CONFIG_FILE = "config.xml"; - private static final String INVALID_CONFIG_FILE = "config.xml.invalid"; - private static final int OPEN_CONFIG_MAX_TRIES = 10; private final Context mContext; @@ -83,19 +81,9 @@ public class ConfigXml { DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder(); mConfig = db.parse(mConfigFile); } catch (SAXException | ParserConfigurationException | IOException e) { - Log.w(TAG, "Failed to open config, moving to " + INVALID_CONFIG_FILE + - " and creating blank config"); - File dest = new File(mConfigFile.getParent(), INVALID_CONFIG_FILE); - if (dest.exists()) - dest.delete(); - mConfigFile.renameTo(dest); - generateKeysConfig(context); - isFirstStart = true; - mConfigFile = getConfigFile(context); + throw new OpenConfigException(); } } - if (mConfig == null) - throw new OpenConfigException(); if (isFirstStart) { changeDefaultFolder(); diff --git a/src/test/java/com/nutomic/syncthingandroid/syncthing/SyncthingServiceTest.java b/src/test/java/com/nutomic/syncthingandroid/syncthing/SyncthingServiceTest.java new file mode 100644 index 00000000..42d7cf2c --- /dev/null +++ b/src/test/java/com/nutomic/syncthingandroid/syncthing/SyncthingServiceTest.java @@ -0,0 +1,106 @@ +package com.nutomic.syncthingandroid.syncthing; + +import android.content.SharedPreferences; +import android.os.Handler; +import android.preference.PreferenceManager; +import com.nutomic.syncthingandroid.BuildConfig; +import com.nutomic.syncthingandroid.util.ConfigXml; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +/** + * These tests assume that syncthing keys have already been generated. If not, tests may fail + * because startup takes too long. + */ +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class) +public class SyncthingServiceTest { + + private SyncthingService mService; + + @Before + public void setup() { + mService = Robolectric.buildService(SyncthingService.class).get(); + mService.onCreate(); + } + + @Test + public void testFirstStart() { + assertTrue(mService.isFirstStart()); + } + + @Test + public void testNotFirstStart() throws IOException { + new File(mService.getFilesDir(), SyncthingService.PUBLIC_KEY_FILE).createNewFile(); + assertFalse(mService.isFirstStart()); + } + + @Test + public void testBindService() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(2); + mService.registerOnWebGuiAvailableListener(new SyncthingService.OnWebGuiAvailableListener() { + @Override + public void onWebGuiAvailable() { + latch.countDown(); + } + }); + mService.registerOnApiChangeListener(new SyncthingService.OnApiChangeListener() { + @Override + public void onApiChange(SyncthingService.State currentState) { + latch.countDown(); + } + }); + latch.await(1, TimeUnit.SECONDS); + assertNotNull(mService); + } + + @Test + public void testImportExportConfig() { + File config = new File(mService.getFilesDir(), ConfigXml.CONFIG_FILE); + File privateKey = new File(mService.getFilesDir(), SyncthingService.PRIVATE_KEY_FILE); + File publicKey = new File(mService.getFilesDir(), SyncthingService.PUBLIC_KEY_FILE); + + try { + config.createNewFile(); + privateKey.createNewFile(); + publicKey.createNewFile(); + } catch (IOException e) { + fail(); + } + + mService.exportConfig(); + + config.delete(); + privateKey.delete(); + publicKey.delete(); + + assertTrue(mService.importConfig()); + assertTrue(config.exists()); + assertTrue(privateKey.exists()); + assertTrue(publicKey.exists()); + } + + @Test + public void testPassword() throws InterruptedException { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mService); + assertNotNull(sp.getString("gui_user", null)); + assertEquals(20, sp.getString("gui_password", null).length()); + } + }, 5000); + } + +}