From 924be98aaa1d252774a41fc4e12e337c7ec5a301 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Wed, 24 Sep 2014 18:18:04 +0300 Subject: [PATCH] Added inotifiy support. --- .../syncthingandroid/test/MockRestApi.java | 14 ++- .../nutomic/syncthingandroid/test/Util.java | 19 +++ .../test/syncthing/RestApiTest.java | 11 +- .../test/syncthing/SyncthingServiceTest.java | 13 +-- .../test/util/RepoObserverTest.java | 93 +++++++++++++++ .../test/util/ReposAdapterTest.java | 2 +- .../syncthingandroid/syncthing/PostTask.java | 2 + .../syncthingandroid/syncthing/RestApi.java | 14 ++- .../syncthing/SyncthingService.java | 11 ++ .../syncthingandroid/util/RepoObserver.java | 108 ++++++++++++++++++ 10 files changed, 264 insertions(+), 23 deletions(-) create mode 100644 src/androidTest/java/com/nutomic/syncthingandroid/test/Util.java create mode 100644 src/androidTest/java/com/nutomic/syncthingandroid/test/util/RepoObserverTest.java create mode 100644 src/main/java/com/nutomic/syncthingandroid/util/RepoObserver.java diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/MockRestApi.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/MockRestApi.java index 4addb0d2..f674bdda 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/MockRestApi.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/MockRestApi.java @@ -69,22 +69,22 @@ public class MockRestApi extends RestApi { } @Override - public void editNode(final Node node, - final OnNodeIdNormalizedListener listener) { + public void editNode(Node node, Activity activity, OnNodeIdNormalizedListener listener) { throw new UnsupportedOperationException(); } @Override - public boolean deleteNode(Node node, Context context) { + public boolean deleteNode(Node node, Activity activity) { throw new UnsupportedOperationException(); } @Override - public boolean editRepo(Repo repo, boolean create, Context context) { + public boolean editRepo(Repo repo, boolean create, Activity activity) { throw new UnsupportedOperationException(); } - public boolean deleteRepo(Repo repo, Context context) { + @Override + public boolean deleteRepo(Repo repo, Activity activity) { throw new UnsupportedOperationException(); } @@ -99,4 +99,8 @@ public class MockRestApi extends RestApi { throw new UnsupportedOperationException(); } + @Override + public void onRepoFileChange(String repoId, String relativePath) { + throw new UnsupportedOperationException(); + } } diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/Util.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/Util.java new file mode 100644 index 00000000..75841ecd --- /dev/null +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/Util.java @@ -0,0 +1,19 @@ +package com.nutomic.syncthingandroid.test; + +import java.io.File; + +public class Util { + + /** + * Deletes the given folder and all contents. + */ + public static void deleteRecursive(File file) { + if (file.isDirectory()) { + for (File f : file.listFiles()) { + deleteRecursive(f); + } + } + file.delete(); + } + +} 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 2f464075..b9d7ce60 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java @@ -1,5 +1,6 @@ package com.nutomic.syncthingandroid.test.syncthing; +import android.app.Activity; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.MediumTest; @@ -146,7 +147,7 @@ public class RestApiTest extends AndroidTestCase { node.Addresses = "dynamic"; node.Name = "my node"; final CountDownLatch latch = new CountDownLatch(1); - mApi.editNode(node, new RestApi.OnNodeIdNormalizedListener() { + mApi.editNode(node, new Activity(), new RestApi.OnNodeIdNormalizedListener() { @Override public void onNodeIdNormalized(String normalizedId, String error) { assertEquals(node.NodeID, normalizedId); @@ -156,7 +157,7 @@ public class RestApiTest extends AndroidTestCase { }); latch.await(10, TimeUnit.SECONDS); - assertTrue(mApi.deleteNode(node, getContext())); + assertTrue(mApi.deleteNode(node, new Activity())); } @SmallTest @@ -164,12 +165,12 @@ public class RestApiTest extends AndroidTestCase { RestApi.Repo repo = new RestApi.Repo(); repo.Directory = "/my/dir"; repo.ID = "my-repo"; - repo.Nodes = new ArrayList<>(); + repo.NodeIds = new ArrayList<>(); repo.ReadOnly = false; repo.Versioning = new RestApi.Versioning(); - assertTrue(mApi.editRepo(repo, true, getContext())); + assertTrue(mApi.editRepo(repo, true, new Activity())); - assertTrue(mApi.deleteRepo(repo, getContext())); + assertTrue(mApi.deleteRepo(repo, new Activity())); } @MediumTest diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/SyncthingServiceTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/SyncthingServiceTest.java index 000996f7..30aa8ec5 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/SyncthingServiceTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/SyncthingServiceTest.java @@ -12,6 +12,7 @@ import com.nutomic.syncthingandroid.syncthing.DeviceStateHolder; 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 java.io.File; import java.io.IOException; @@ -41,21 +42,11 @@ public class SyncthingServiceTest extends ServiceTestCase { @Override protected void tearDown() throws Exception { - deleteRecursive(getContext().getFilesDir()); + Util.deleteRecursive(getContext().getFilesDir()); PreferenceManager.getDefaultSharedPreferences(getContext()).edit().clear().commit(); - super.tearDown(); } - private void deleteRecursive(File file) { - if (file.isDirectory()) { - for (File f : file.listFiles()) { - deleteRecursive(f); - } - } - file.delete(); - } - @LargeTest public void testStartService() throws InterruptedException { startService(new Intent(mContext, SyncthingService.class)); diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/RepoObserverTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/RepoObserverTest.java new file mode 100644 index 00000000..7eb43646 --- /dev/null +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/RepoObserverTest.java @@ -0,0 +1,93 @@ +package com.nutomic.syncthingandroid.test.util; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.MediumTest; + +import com.nutomic.syncthingandroid.syncthing.RestApi; +import com.nutomic.syncthingandroid.test.MockContext; +import com.nutomic.syncthingandroid.test.Util; +import com.nutomic.syncthingandroid.util.RepoObserver; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class RepoObserverTest extends AndroidTestCase + implements RepoObserver.OnRepoFileChangeListener { + + private File mTestFolder; + + private String mCurrentTest; + + private CountDownLatch mLatch; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mTestFolder = new File(new MockContext(getContext()).getFilesDir(), "observer-test"); + mTestFolder.mkdir(); + } + + @Override + protected void tearDown() throws Exception { + Util.deleteRecursive(mTestFolder); + super.tearDown(); + } + + @Override + public void onRepoFileChange(String repoId, String relativePath) { + mLatch.countDown(); + assertEquals(mCurrentTest, repoId); + assertFalse(relativePath.endsWith("should-not-notifiy")); + } + + private RestApi.Repo createRepo(String id) { + RestApi.Repo r = new RestApi.Repo(); + r.Directory = mTestFolder.getAbsolutePath(); + r.ID = id; + return r; + } + + @MediumTest + public void testRecursion() throws IOException, InterruptedException { + mCurrentTest = "testRecursion"; + File subFolder = new File(mTestFolder, "subfolder"); + subFolder.mkdir(); + RepoObserver ro = new RepoObserver(this, createRepo(mCurrentTest)); + File testFile = new File(subFolder, "test"); + mLatch = new CountDownLatch(1); + testFile.createNewFile(); + mLatch.await(1, TimeUnit.SECONDS); + ro.stopWatching(); + } + + @MediumTest + public void testRemoveDirectory() throws IOException { + mCurrentTest = "testRemoveDirectory"; + File subFolder = new File(mTestFolder, "subfolder"); + subFolder.mkdir(); + RepoObserver ro = new RepoObserver(this, createRepo(mCurrentTest)); + File movedSubFolder = new File(getContext().getFilesDir(), subFolder.getName()); + subFolder.renameTo(movedSubFolder); + File testFile = new File(movedSubFolder, "should-not-notifiy"); + mLatch = new CountDownLatch(1); + testFile.createNewFile(); + ro.stopWatching(); + Util.deleteRecursive(subFolder); + } + + @MediumTest + public void testAddDirectory() throws IOException, InterruptedException { + mCurrentTest = "testAddDirectory"; + File subFolder = new File(mTestFolder, "subfolder"); + RepoObserver ro = new RepoObserver(this, createRepo(mCurrentTest)); + subFolder.mkdir(); + File testFile = new File(subFolder, "test"); + mLatch = new CountDownLatch(1); + testFile.createNewFile(); + mLatch.await(1, TimeUnit.SECONDS); + ro.stopWatching(); + } + +} diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ReposAdapterTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ReposAdapterTest.java index 3375b16c..e3b6285f 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ReposAdapterTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ReposAdapterTest.java @@ -28,7 +28,7 @@ public class ReposAdapterTest extends AndroidTestCase { mRepo.Directory = "/my/dir/"; mRepo.ID = "id 123"; mRepo.Invalid = "all good"; - mRepo.Nodes = new ArrayList<>(); + mRepo.NodeIds = new ArrayList<>(); mRepo.ReadOnly = false; mRepo.Versioning = new RestApi.Versioning(); diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java index f2b38518..1d39c39c 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java @@ -27,6 +27,8 @@ public class PostTask extends AsyncTask { public static final String URI_SHUTDOWN = "/rest/shutdown"; + public static final String URI_SCAN = "/rest/scan"; + /** * params[0] Syncthing hostname * params[1] URI to call diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java index 592020ed..801f2528 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java @@ -18,11 +18,13 @@ import android.widget.Toast; import com.nutomic.syncthingandroid.BuildConfig; import com.nutomic.syncthingandroid.R; +import com.nutomic.syncthingandroid.util.RepoObserver; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.io.File; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.HashMap; @@ -33,7 +35,8 @@ import java.util.concurrent.atomic.AtomicInteger; /** * Provides functions to interact with the syncthing REST API. */ -public class RestApi implements SyncthingService.OnWebGuiAvailableListener { +public class RestApi implements SyncthingService.OnWebGuiAvailableListener, + RepoObserver.OnRepoFileChangeListener { private static final String TAG = "RestApi"; @@ -887,4 +890,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { .show(); } + /** + * Force a rescan of the given subdirectory in repository. + */ + @Override + public void onRepoFileChange(String repoId, String relativePath) { + new PostTask().execute(mUrl, PostTask.URI_SCAN, mApiKey, "repo", repoId, "sub", + relativePath); + } + } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java index e2f57e12..80d9ea6d 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java @@ -15,12 +15,14 @@ import android.util.Log; import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.activities.SettingsActivity; import com.nutomic.syncthingandroid.util.ConfigXml; +import com.nutomic.syncthingandroid.util.RepoObserver; import java.io.File; import java.io.FilenameFilter; import java.lang.ref.WeakReference; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedList; /** * Holds the native syncthing instance and provides an API to access it. @@ -62,6 +64,8 @@ public class SyncthingService extends Service { private RestApi mApi; + private LinkedList mObservers = new LinkedList<>(); + private SyncthingRunnable mSyncthingRunnable; private final SyncthingServiceBinder mBinder = new SyncthingServiceBinder(this); @@ -168,6 +172,10 @@ public class SyncthingService extends Service { mStopScheduled = true; } else if (mApi != null) { mApi.shutdown(); + for (RepoObserver ro : mObservers) { + ro.stopWatching(); + } + mObservers.clear(); } } onApiChange(); @@ -241,6 +249,9 @@ public class SyncthingService extends Service { @Override public void onApiAvailable() { onApiChange(); + for (RestApi.Repo r : mApi.getRepos()) { + mObservers.add(new RepoObserver(mApi, r)); + } } }); registerOnWebGuiAvailableListener(mApi); diff --git a/src/main/java/com/nutomic/syncthingandroid/util/RepoObserver.java b/src/main/java/com/nutomic/syncthingandroid/util/RepoObserver.java new file mode 100644 index 00000000..4482a2dd --- /dev/null +++ b/src/main/java/com/nutomic/syncthingandroid/util/RepoObserver.java @@ -0,0 +1,108 @@ +package com.nutomic.syncthingandroid.util; + +import android.os.FileObserver; +import android.util.Log; + +import com.nutomic.syncthingandroid.syncthing.RestApi; + +import java.io.File; +import java.io.FilenameFilter; +import java.util.ArrayList; + +/** + * Recursively watches a directory and all subfolders. + */ +public class RepoObserver extends FileObserver { + + private static final String TAG = "RepoObserver"; + + private final OnRepoFileChangeListener mListener; + + private final RestApi.Repo mRepo; + + private final String mPath; + + private final ArrayList mChilds; + + public interface OnRepoFileChangeListener { + public void onRepoFileChange(String repoId, String relativePath); + } + + public RepoObserver(OnRepoFileChangeListener listener, RestApi.Repo repo) { + this(listener, repo, ""); + } + + /** + * Constructs watcher and starts watching the given directory recursively. + * + * @param listener The listener where changes should be sent to. + * @param repo The repository where this folder belongs to. + * @param path Path to the monitored folder, relative to repo root. + */ + private RepoObserver(OnRepoFileChangeListener listener, RestApi.Repo repo, String path) { + super(repo.Directory + "/" + path, + ATTRIB | CLOSE_WRITE | CREATE | DELETE | DELETE_SELF | MODIFY | MOVED_FROM | + MOVED_TO | MOVE_SELF); + mListener = listener; + mRepo = repo; + mPath = path; + Log.v(TAG, "observer created for " + path + " in " + repo.ID); + startWatching(); + + File[] directories = new File(repo.Directory, path).listFiles(new FilenameFilter() { + @Override + public boolean accept(File current, String name) { + return new File(current, name).isDirectory(); + } + }); + + mChilds = new ArrayList<>(directories.length); + for (File f : directories) { + mChilds.add(new RepoObserver(mListener, mRepo, path + "/" + f.getName())); + } + } + + /** + * Handles incoming events for changed files. + */ + @Override + public void onEvent(int event, String path) { + // Ignore some weird events that we may receive. + event &= FileObserver.ALL_EVENTS; + if (event == 0) + return; + + switch (event) { + case MOVED_FROM: + // fall through + case DELETE_SELF: + // fall through + case DELETE: + for (RepoObserver ro : mChilds) { + if (ro.mPath.equals(path)) { + mChilds.remove(ro); + break; + } + } + break; + case MOVED_TO: + // fall through + case CREATE: + mChilds.add(new RepoObserver(mListener, mRepo, path)); + // fall through + default: + mListener.onRepoFileChange(mRepo.ID, new File(mPath, path).getPath()); + } + } + + /** + * Recursively stops watching the directory. + */ + @Override + public void stopWatching() { + super.stopWatching(); + for (RepoObserver ro : mChilds) { + ro.stopWatching(); + } + } +}