From c21da40784a1be653a6a4f42cb7333c510d3c998 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Fri, 26 Jul 2024 07:58:32 +0200 Subject: [PATCH 01/18] Ensure that Git repo file paths start with a slash Because this is what they look like according to Git, and we don't want our remote book file "URL" to look different from the repo's view. --- app/src/main/java/com/orgzly/android/repos/GitRepo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/orgzly/android/repos/GitRepo.java b/app/src/main/java/com/orgzly/android/repos/GitRepo.java index 4bd759889..5cd0b585b 100644 --- a/app/src/main/java/com/orgzly/android/repos/GitRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/GitRepo.java @@ -208,7 +208,7 @@ RevCommit getCommitFromRevisionString(String revisionString) throws IOException @Override public VersionedRook retrieveBook(String fileName, File destination) throws IOException { - Uri sourceUri = Uri.parse(fileName); + Uri sourceUri = Uri.parse("/" + fileName); // Ensure our repo copy is up-to-date. This is necessary when force-loading a book. synchronizer.mergeWithRemote(); From a2a414a8858ec100777b0735cb6658b26c8288d8 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Tue, 16 Jul 2024 17:48:12 +0200 Subject: [PATCH 02/18] Fix incorrect mtime stored for VersionedRooks in ContentRepo It seems completely wrong to store the current system time as "mtime", instead of the actual "last modified" filesystem metadata. --- app/src/main/java/com/orgzly/android/repos/ContentRepo.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/orgzly/android/repos/ContentRepo.java b/app/src/main/java/com/orgzly/android/repos/ContentRepo.java index ef87d2ac2..dc80a0153 100644 --- a/app/src/main/java/com/orgzly/android/repos/ContentRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/ContentRepo.java @@ -171,8 +171,8 @@ public VersionedRook storeBook(File file, String fileName) throws IOException { } } - String rev = String.valueOf(destinationFile.lastModified()); - long mtime = System.currentTimeMillis(); + long mtime = destinationFile.lastModified(); + String rev = String.valueOf(mtime); return new VersionedRook(repoId, RepoType.DOCUMENT, getUri(), uri, rev, mtime); } From 43d88fdb750e14eabe86bd821713aa21f9bc2f4b Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Thu, 20 Jun 2024 00:52:47 +0200 Subject: [PATCH 03/18] Add subfolder support in ContentRepo - Loading from subdirectories works - Creating new subdirectories works - Renaming across subdirectories works --- .../java/com/orgzly/android/BookName.java | 15 ++ .../com/orgzly/android/data/DataRepository.kt | 2 +- .../com/orgzly/android/repos/ContentRepo.java | 173 +++++++++++++----- .../com/orgzly/android/sync/BookNamesake.java | 2 +- .../java/com/orgzly/android/sync/SyncUtils.kt | 3 +- .../com/orgzly/android/util/MiscUtils.java | 21 ++- app/src/main/res/values/strings.xml | 2 + 7 files changed, 171 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/orgzly/android/BookName.java b/app/src/main/java/com/orgzly/android/BookName.java index 139ea2d7e..aeee48304 100644 --- a/app/src/main/java/com/orgzly/android/BookName.java +++ b/app/src/main/java/com/orgzly/android/BookName.java @@ -62,6 +62,21 @@ public static String getFileName(Context context, Uri uri) { return fileName; } + public static String getFileName(Uri repoUri, Uri fileUri) { + /* The content:// repository type requires special handling */ + if ("content".equals(repoUri.getScheme())) { + String repoUriLastSegment = repoUri.toString().replaceAll("^.*/", ""); + String repoRootUriSegment = repoUri + "/document/" + repoUriLastSegment + "%2F"; + return Uri.decode(fileUri.toString().replace(repoRootUriSegment, "")); + } else { + // Just return the decoded fileUri stripped of the repoUri (if present), and stripped + // of any leading / (if present). + return Uri.decode( + fileUri.toString().replace(repoUri.toString(), "") + ).replaceFirst("^/", ""); + } + } + public static BookName getInstance(Context context, Rook rook) { return fromFileName(getFileName(context, rook.getUri())); } diff --git a/app/src/main/java/com/orgzly/android/data/DataRepository.kt b/app/src/main/java/com/orgzly/android/data/DataRepository.kt index a4f08fcbf..5127448cc 100644 --- a/app/src/main/java/com/orgzly/android/data/DataRepository.kt +++ b/app/src/main/java/com/orgzly/android/data/DataRepository.kt @@ -1625,7 +1625,7 @@ class DataRepository @Inject constructor( @Throws(IOException::class) fun loadBookFromRepo(rook: Rook): BookView? { - val fileName = BookName.getFileName(context, rook.uri) + val fileName = BookName.getFileName(rook.repoUri, rook.uri) return loadBookFromRepo(rook.repoId, rook.repoType, rook.repoUri.toString(), fileName) } diff --git a/app/src/main/java/com/orgzly/android/repos/ContentRepo.java b/app/src/main/java/com/orgzly/android/repos/ContentRepo.java index dc80a0153..f56da3bf8 100644 --- a/app/src/main/java/com/orgzly/android/repos/ContentRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/ContentRepo.java @@ -9,6 +9,7 @@ import androidx.documentfile.provider.DocumentFile; import com.orgzly.BuildConfig; +import com.orgzly.R; import com.orgzly.android.BookName; import com.orgzly.android.db.entity.Repo; import com.orgzly.android.util.LogUtils; @@ -20,6 +21,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -68,20 +70,11 @@ public Uri getUri() { public List getBooks() throws IOException { List result = new ArrayList<>(); - DocumentFile[] files = repoDocumentFile.listFiles(); - - RepoIgnoreNode ignores = new RepoIgnoreNode(this); - - if (files != null) { - // Can't compare TreeDocumentFile - // Arrays.sort(files); + List files = walkFileTree(); + if (files.size() > 0) { for (DocumentFile file : files) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (ignores.isPathIgnored(Objects.requireNonNull(file.getName()), false)) { - continue; - } - } if (BookName.isSupportedFormatFileName(file.getName())) { + if (BookName.isSupportedFormatFileName(file.getName())) { if (BuildConfig.LOG_DEBUG) { LogUtils.d(TAG, @@ -111,9 +104,45 @@ public List getBooks() throws IOException { return result; } + /** + * @return All file nodes in the repo tree which are not excluded by .orgzlyignore + */ + private List walkFileTree() { + List result = new ArrayList<>(); + List directoryNodes = new ArrayList<>(); + RepoIgnoreNode ignores = new RepoIgnoreNode(this); + directoryNodes.add(repoDocumentFile); + while (!directoryNodes.isEmpty()) { + DocumentFile currentDir = directoryNodes.remove(0); + for (DocumentFile node : currentDir.listFiles()) { + String relativeFileName = BookName.getFileName(repoUri, node.getUri()); + if (node.isDirectory()) { + if (Build.VERSION.SDK_INT >= 26) { + if (ignores.isPathIgnored(relativeFileName, true)) { + continue; + } + } + directoryNodes.add(node); + } else { + if (Build.VERSION.SDK_INT >= 26) { + if (ignores.isPathIgnored(relativeFileName, false)) { + continue; + } + } result.add(node); + } + } + } + return result; + } + + private DocumentFile getDocumentFileFromFileName(String fileName) { + String fullUri = repoDocumentFile.getUri() + Uri.encode("/" + fileName); + return DocumentFile.fromSingleUri(context, Uri.parse(fullUri)); + } + @Override public VersionedRook retrieveBook(String fileName, File destinationFile) throws IOException { - DocumentFile sourceFile = repoDocumentFile.findFile(fileName); + DocumentFile sourceFile = getDocumentFileFromFileName(fileName); if (sourceFile == null) { throw new FileNotFoundException("Book " + fileName + " not found in " + repoUri); } else { @@ -135,8 +164,8 @@ public VersionedRook retrieveBook(String fileName, File destinationFile) throws @Override public InputStream openRepoFileInputStream(String fileName) throws IOException { - DocumentFile sourceFile = repoDocumentFile.findFile(fileName); - if (sourceFile == null) throw new FileNotFoundException(); + DocumentFile sourceFile = getDocumentFileFromFileName(fileName); + if (!sourceFile.exists()) throw new FileNotFoundException(); return context.getContentResolver().openInputStream(sourceFile.getUri()); } @@ -145,24 +174,17 @@ public VersionedRook storeBook(File file, String fileName) throws IOException { if (!file.exists()) { throw new FileNotFoundException("File " + file + " does not exist"); } - - /* Delete existing file. */ - DocumentFile existingFile = repoDocumentFile.findFile(fileName); - if (existingFile != null) { - existingFile.delete(); - } - - /* Create new file. */ - DocumentFile destinationFile = repoDocumentFile.createFile("text/*", fileName); - - if (destinationFile == null) { - throw new IOException("Failed creating " + fileName + " in " + repoUri); + DocumentFile destinationFile = getDocumentFileFromFileName(fileName); + if (!destinationFile.exists()) { + if (fileName.contains("/")) { + DocumentFile destinationDir = ensureDirectoryHierarchy(fileName); + destinationFile = destinationDir.createFile("text/*", Uri.parse(fileName).getLastPathSegment()); + } else { + repoDocumentFile.createFile("text/*", fileName); + } } + OutputStream out = context.getContentResolver().openOutputStream(destinationFile.getUri()); - Uri uri = destinationFile.getUri(); - - /* Write file content to uri. */ - OutputStream out = context.getContentResolver().openOutputStream(uri); try { MiscUtils.writeFileToStream(file, out); } finally { @@ -174,27 +196,94 @@ public VersionedRook storeBook(File file, String fileName) throws IOException { long mtime = destinationFile.lastModified(); String rev = String.valueOf(mtime); - return new VersionedRook(repoId, RepoType.DOCUMENT, getUri(), uri, rev, mtime); + return new VersionedRook(repoId, RepoType.DOCUMENT, getUri(), destinationFile.getUri(), rev, mtime); + } + + /** + * Given a relative path, ensures that all directory levels are created unless they already + * exist. + * @param relativePath Path relative to the repository root directory + * @return The DocumentFile object of the leaf directory where the file should be placed. + */ + private DocumentFile ensureDirectoryHierarchy(String relativePath) { + List levels = new ArrayList<>(Arrays.asList(relativePath.split("/"))); + DocumentFile currentDir = repoDocumentFile; + while (levels.size() > 1) { + String nextDirName = levels.remove(0); + DocumentFile nextDir = currentDir.findFile(nextDirName); + if (nextDir == null) { + currentDir = currentDir.createDirectory(nextDirName); + } else { + currentDir = nextDir; + } + } + return currentDir; } + /** + * Allows renaming a notebook to any subdirectory (indicated with a "/"), ensuring that all + * required subdirectories are created, if they do not already exist. Note that the file is + * moved, but no "abandoned" directories are deleted. + * @param oldUri + * @param newName + * @return + * @throws IOException + */ @Override - public VersionedRook renameBook(Uri from, String name) throws IOException { - DocumentFile fromDocFile = DocumentFile.fromSingleUri(context, from); - BookName bookName = BookName.fromFileName(fromDocFile.getName()); - String newFileName = BookName.fileName(name, bookName.getFormat()); + public VersionedRook renameBook(Uri oldUri, String newName) throws IOException { + DocumentFile oldDocFile = DocumentFile.fromSingleUri(context, oldUri); + long mtime = oldDocFile.lastModified(); + String rev = String.valueOf(mtime); + String oldDocFileName = oldDocFile.getName(); + Uri oldDirUri = Uri.parse( + oldUri.toString().replace( + Uri.encode("/" + oldDocFile.getName()), + "" + ) + ); + BookName oldBookName = BookName.fromFileName(BookName.getFileName(repoUri, oldUri)); + String newRelativePath = BookName.fileName(newName, oldBookName.getFormat()); + String newDocFileName = Uri.parse(newRelativePath).getLastPathSegment(); + DocumentFile newDir; + Uri newUri = oldUri; + + if (newName.contains("/")) { + newDir = ensureDirectoryHierarchy(newName); + } else { + newDir = repoDocumentFile; + } - /* Check if document already exists. */ - DocumentFile existingFile = repoDocumentFile.findFile(newFileName); + /* Abort if destination file already exists. */ + DocumentFile existingFile = newDir.findFile(newDocFileName); if (existingFile != null) { throw new IOException("File at " + existingFile.getUri() + " already exists"); } - Uri newUri = DocumentsContract.renameDocument(context.getContentResolver(), from, newFileName); + if (!newDir.getUri().toString().equals(oldDirUri.toString())) { + // File should be moved to a different directory + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + newUri = DocumentsContract.moveDocument( + context.getContentResolver(), + oldUri, + oldDirUri, + newDir.getUri() + ); + } else { + throw new IllegalArgumentException( + context.getString(R.string.moving_between_subdirectories_requires_api_24)); + } + } - long mtime = fromDocFile.lastModified(); - String rev = String.valueOf(mtime); + if (!Objects.equals(newDocFileName, oldDocFileName)) { + // File should be renamed + newUri = DocumentsContract.renameDocument( + context.getContentResolver(), + newUri, + newDocFileName + ); + } - return new VersionedRook(repoId, RepoType.DOCUMENT, getUri(), newUri, rev, mtime); + return new VersionedRook(repoId, RepoType.DOCUMENT, repoUri, newUri, rev, mtime); } @Override diff --git a/app/src/main/java/com/orgzly/android/sync/BookNamesake.java b/app/src/main/java/com/orgzly/android/sync/BookNamesake.java index a5c3271e1..ef49d2ea7 100644 --- a/app/src/main/java/com/orgzly/android/sync/BookNamesake.java +++ b/app/src/main/java/com/orgzly/android/sync/BookNamesake.java @@ -49,7 +49,7 @@ public static Map getAll(Context context, List b /* Set repo books. */ for (VersionedRook book: versionedRooks) { - String fileName = BookName.getFileName(context, book.getUri()); + String fileName = BookName.getFileName(book.getRepoUri(), book.getUri()); String name = BookName.fromFileName(fileName).getName(); BookNamesake pair = namesakes.get(name); diff --git a/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt b/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt index 714592a11..6031f13f2 100644 --- a/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt +++ b/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt @@ -1,5 +1,6 @@ package com.orgzly.android.sync +import androidx.core.net.toUri import com.orgzly.BuildConfig import com.orgzly.android.App import com.orgzly.android.BookFormat @@ -166,7 +167,7 @@ object SyncUtils { BookSyncStatus.BOOK_WITH_LINK_LOCAL_MODIFIED -> { repoEntity = namesake.book.linkRepo repoUrl = repoEntity!!.url - fileName = BookName.getFileName(App.getAppContext(), namesake.book.syncedTo!!.uri) + fileName = BookName.getFileName(repoUrl.toUri(), namesake.book.syncedTo!!.uri) dataRepository.saveBookToRepo(repoEntity, fileName, namesake.book, BookFormat.ORG) bookAction = BookAction.forNow(BookAction.Type.INFO, namesake.status.msg(repoUrl)) } diff --git a/app/src/main/java/com/orgzly/android/util/MiscUtils.java b/app/src/main/java/com/orgzly/android/util/MiscUtils.java index 4e8414016..8546cb7d8 100644 --- a/app/src/main/java/com/orgzly/android/util/MiscUtils.java +++ b/app/src/main/java/com/orgzly/android/util/MiscUtils.java @@ -1,15 +1,18 @@ package com.orgzly.android.util; +import android.content.ContentResolver; import android.net.Uri; -import com.google.android.material.textfield.TextInputLayout; import android.text.Editable; -import android.text.Html; import android.text.Spanned; import android.text.TextWatcher; import android.widget.TextView; import androidx.core.text.HtmlCompat; +import androidx.documentfile.provider.DocumentFile; + +import com.google.android.material.textfield.TextInputLayout; +import com.orgzly.android.App; import java.io.BufferedReader; import java.io.File; @@ -69,6 +72,20 @@ public static void writeStringToFile(String str, File file) throws FileNotFoundE } } + public static void writeStringToDocumentFile(String content, String displayName, Uri directory) throws IOException { + DocumentFile file = DocumentFile.fromTreeUri(App.getAppContext(), directory) + .createFile("", displayName); + ContentResolver contentResolver = App.getAppContext().getContentResolver(); + try (OutputStream out = contentResolver.openOutputStream(file.getUri())) { + if (out != null) { + out.write(content.getBytes()); + out.flush(); + } else { + throw new IOException("Failed to open output stream for writing to " + file.getUri()); + } + } + } + /** * Compares content of two text files. * @return null if files match, difference if they don't diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e67fec24d..c26eeac1d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -782,4 +782,6 @@ Notebook has no repository link Loaded from %s Saved to %s + + Renaming notebook to a different subdirectory requires Android 7 or higher From 108b3b8fcda3dd1d7cadc710361093c3c791a837 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Thu, 11 Jul 2024 11:03:06 +0200 Subject: [PATCH 04/18] Remove unused argument from utility method signature --- .../android/espresso/BookPrefaceTest.kt | 4 +- .../android/espresso/BooksSortOrderTest.kt | 4 +- .../espresso/CreatedAtPropertyTest.java | 8 +-- .../com/orgzly/android/espresso/MiscTest.java | 4 +- .../android/espresso/NoteFragmentTest.kt | 8 +-- .../android/espresso/SettingsChangeTest.java | 8 +-- .../espresso/SettingsFragmentTest.java | 52 +++++++++---------- .../android/espresso/SshKeyCreationTest.kt | 10 ++-- .../orgzly/android/espresso/SyncingTest.java | 16 +++--- .../android/espresso/util/EspressoUtils.java | 6 +-- .../util/ScreenshotsTakingNotATest.kt | 4 +- 11 files changed, 62 insertions(+), 62 deletions(-) diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/BookPrefaceTest.kt b/app/src/androidTest/java/com/orgzly/android/espresso/BookPrefaceTest.kt index 8ed959efd..c9e61188d 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/BookPrefaceTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/espresso/BookPrefaceTest.kt @@ -84,8 +84,8 @@ class BookPrefaceTest : OrgzlyTest() { private fun setPrefaceSetting(@StringRes id: Int) { onActionItemClick(R.id.activity_action_settings, R.string.settings) - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks) - clickSetting("pref_key_preface_in_book", R.string.preface_in_book) + clickSetting(R.string.pref_title_notebooks) + clickSetting(R.string.preface_in_book) onView(withText(id)).perform(click()) diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/BooksSortOrderTest.kt b/app/src/androidTest/java/com/orgzly/android/espresso/BooksSortOrderTest.kt index f88468432..f3d546c08 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/BooksSortOrderTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/espresso/BooksSortOrderTest.kt @@ -60,8 +60,8 @@ class BooksSortOrderTest : OrgzlyTest() { private fun setBooksSortOrder(@StringRes id: Int) { onActionItemClick(R.id.activity_action_settings, R.string.settings) - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks) - clickSetting("pref_key_notebooks_sort_order", R.string.sort_order) + clickSetting(R.string.pref_title_notebooks) + clickSetting(R.string.sort_order) onData(hasToString(context.getString(id))).perform(click()) pressBack() pressBack() diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/CreatedAtPropertyTest.java b/app/src/androidTest/java/com/orgzly/android/espresso/CreatedAtPropertyTest.java index 93d88d82c..79791febb 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/CreatedAtPropertyTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/CreatedAtPropertyTest.java @@ -136,8 +136,8 @@ public void testNewNote() { private void enableCreatedAt() { onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_sync", R.string.sync); - clickSetting("pref_key_is_created_at_added", R.string.use_created_at_property); + clickSetting(R.string.sync); + clickSetting(R.string.use_created_at_property); onView(withText(R.string.yes)).perform(click()); pressBack(); pressBack(); @@ -145,8 +145,8 @@ private void enableCreatedAt() { private void changeCreatedAtProperty(String propName) { onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_sync", R.string.sync); - clickSetting("pref_key_created_at_property", R.string.created_at_property); + clickSetting(R.string.sync); + clickSetting(R.string.created_at_property); onView(instanceOf(EditText.class)).perform(replaceTextCloseKeyboard(propName)); onView(withText(android.R.string.ok)).perform(click()); onView(withText(R.string.yes)).perform(click()); diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/MiscTest.java b/app/src/androidTest/java/com/orgzly/android/espresso/MiscTest.java index f3ab040de..1c7ba9bf6 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/MiscTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/MiscTest.java @@ -108,8 +108,8 @@ public void testClearDatabaseWithFragmentsInBackStack() { onView(allOf(withText("book-two"), isDisplayed())).perform(click()); onView(withText("Note #2.")).perform(click()); onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_app", R.string.app); - clickSetting("pref_key_clear_database", R.string.clear_database); + clickSetting(R.string.app); + clickSetting(R.string.clear_database); onView(withText(R.string.ok)).perform(click()); pressBack(); pressBack(); diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/NoteFragmentTest.kt b/app/src/androidTest/java/com/orgzly/android/espresso/NoteFragmentTest.kt index 8a0d73b20..236b3b14d 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/NoteFragmentTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/espresso/NoteFragmentTest.kt @@ -419,8 +419,8 @@ class NoteFragmentTest : OrgzlyTest() { /* Change lowest priority to A. */ onActionItemClick(R.id.activity_action_settings, R.string.settings) - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks) - clickSetting("pref_key_min_priority", R.string.lowest_priority) + clickSetting(R.string.pref_title_notebooks) + clickSetting(R.string.lowest_priority) onData(hasToString(containsString("A"))).perform(click()) pressBack() pressBack() @@ -431,8 +431,8 @@ class NoteFragmentTest : OrgzlyTest() { /* Change lowest priority to C. */ onActionItemClick(R.id.activity_action_settings, R.string.settings) - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks) - clickSetting("pref_key_min_priority", R.string.lowest_priority) + clickSetting(R.string.pref_title_notebooks) + clickSetting(R.string.lowest_priority) onData(hasToString(containsString("C"))).perform(click()) pressBack() pressBack() diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java b/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java index 6ed707392..b2861cf18 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java @@ -86,8 +86,8 @@ public void testDisplayedContentInBook() { .check(matches(allOf(withText(containsString("Content for [a-1]")), isDisplayed()))); onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks); - clickSetting("pref_key_is_notes_content_displayed_in_list", R.string.display_content); + clickSetting(R.string.pref_title_notebooks); + clickSetting(R.string.display_content); pressBack(); pressBack(); @@ -96,8 +96,8 @@ public void testDisplayedContentInBook() { private void setDefaultPriority(String priority) { onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks); - clickSetting("pref_key_default_priority", R.string.default_priority); + clickSetting(R.string.pref_title_notebooks); + clickSetting(R.string.default_priority); onData(hasToString(containsString(priority))).perform(click()); pressBack(); pressBack(); diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/SettingsFragmentTest.java b/app/src/androidTest/java/com/orgzly/android/espresso/SettingsFragmentTest.java index afffa075e..6241cc7a3 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/SettingsFragmentTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/SettingsFragmentTest.java @@ -39,16 +39,16 @@ public void setUp() throws Exception { @Test public void testImportingGettingStartedFromGettingStartedNotebook() { onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_app", R.string.app); - clickSetting("pref_key_reload_getting_started", R.string.reload_getting_started); + clickSetting(R.string.app); + clickSetting(R.string.reload_getting_started); pressBack(); pressBack(); onView(withId(R.id.fragment_books_view_flipper)).check(matches(isDisplayed())); onView(allOf(withText(R.string.getting_started_notebook_name), isDisplayed())).perform(click()); onView(withId(R.id.fragment_book_view_flipper)).check(matches(isDisplayed())); onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_app", R.string.app); - clickSetting("pref_key_reload_getting_started", R.string.reload_getting_started); + clickSetting(R.string.app); + clickSetting(R.string.reload_getting_started); pressBack(); pressBack(); onView(withId(R.id.fragment_book_view_flipper)).check(matches(isDisplayed())); @@ -60,15 +60,15 @@ public void testImportingGettingStartedFromGettingStartedNotebook() { public void testAddingNewTodoKeywordInSettingsAndChangingStateToItForNewNote() { onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks); - clickSetting("pref_key_states", R.string.states); + clickSetting(R.string.pref_title_notebooks); + clickSetting(R.string.states); onView(withId(R.id.todo_states)).perform(replaceTextCloseKeyboard("TODO AAA BBB CCC")); onView(withText(android.R.string.ok)).perform(click()); onView(withText(R.string.not_now)).perform(click()); - clickSetting("pref_key_new_note_state", R.string.state); + clickSetting(R.string.state); onData(hasToString(containsString("CCC"))).perform(click()); } @@ -77,14 +77,14 @@ public void testAddingNewTodoKeywordInSettingsAndChangingStateToItForNewNote() { public void testAddingNewTodoKeywordInSettingsNewNoteShouldHaveDefaultState() { onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks); - clickSetting("pref_key_states", R.string.states); + clickSetting(R.string.pref_title_notebooks); + clickSetting(R.string.states); onView(withId(R.id.todo_states)).perform(replaceTextCloseKeyboard("TODO CCC")); onView(withText(android.R.string.ok)).perform(click()); onView(withText(R.string.not_now)).perform(click()); - clickSetting("pref_key_new_note_state", R.string.state); + clickSetting(R.string.state); onData(hasToString(containsString("NOTE"))).perform(click()); } @@ -94,8 +94,8 @@ public void testStateSummaryAfterNoStates() { AppPreferences.states(context, "|"); onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks); - clickSetting("pref_key_states", R.string.states); + clickSetting(R.string.pref_title_notebooks); + clickSetting(R.string.states); onView(withId(R.id.todo_states)).perform(replaceTextCloseKeyboard("TODO")); onView(withText(android.R.string.ok)).perform(click()); onView(withText(R.string.not_now)).perform(click()); @@ -106,8 +106,8 @@ public void testStateSummaryAfterNoStates() { public void testStatesDuplicateDetectedIgnoringCase() { onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks); - clickSetting("pref_key_states", R.string.states); + clickSetting(R.string.pref_title_notebooks); + clickSetting(R.string.states); onView(withId(R.id.todo_states)).perform(replaceTextCloseKeyboard("TODO NEXT next")); @@ -121,8 +121,8 @@ public void testNewNoteDefaultStateIsInitiallyVisibleInSummary() { AppPreferences.newNoteState(context, "BBB"); onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks); - clickSetting("pref_key_new_note_state", R.string.state); + clickSetting(R.string.pref_title_notebooks); + clickSetting(R.string.state); onView(withText("BBB")).check(matches(isDisplayed())); } @@ -133,8 +133,8 @@ public void testNewNoteDefaultStateIsSetInitially() { AppPreferences.newNoteState(context, "BBB"); onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks); - clickSetting("pref_key_new_note_state", R.string.state); + clickSetting(R.string.pref_title_notebooks); + clickSetting(R.string.state); onData(hasToString(containsString("BBB"))).perform(click()); } @@ -145,11 +145,11 @@ public void testDefaultPriorityUpdateOnLowestPriorityChange() { AppPreferences.minPriority(context, "E"); onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks); - clickSetting("pref_key_min_priority", R.string.lowest_priority); + clickSetting(R.string.pref_title_notebooks); + clickSetting(R.string.lowest_priority); onData(hasToString(containsString("B"))).perform(click()); - clickSetting("pref_key_default_priority", R.string.default_priority); + clickSetting(R.string.default_priority); onData(hasToString("B")).check(matches(isChecked())); } @@ -159,11 +159,11 @@ public void testLowestPriorityUpdateOnDefaultPriorityChange() { AppPreferences.minPriority(context, "E"); onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks); - clickSetting("pref_key_default_priority", R.string.default_priority); + clickSetting(R.string.pref_title_notebooks); + clickSetting(R.string.default_priority); onData(hasToString(containsString("X"))).perform(click()); - clickSetting("pref_key_min_priority", R.string.lowest_priority); + clickSetting(R.string.lowest_priority); onData(hasToString("X")).check(matches(isChecked())); } @@ -171,8 +171,8 @@ public void testLowestPriorityUpdateOnDefaultPriorityChange() { public void testLowercaseStateConvertedToUppercase() { onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks); - clickSetting("pref_key_states", R.string.states); + clickSetting(R.string.pref_title_notebooks); + clickSetting(R.string.states); onView(withId(R.id.todo_states)).perform(replaceTextCloseKeyboard("TODO NEXT wait")); diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/SshKeyCreationTest.kt b/app/src/androidTest/java/com/orgzly/android/espresso/SshKeyCreationTest.kt index 62cda5c23..0d230f386 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/SshKeyCreationTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/espresso/SshKeyCreationTest.kt @@ -40,13 +40,13 @@ class SshKeyCreationTest(private val param: Parameter) : OrgzlyTest() { Assume.assumeFalse(BuildConfig.IS_GIT_REMOVED); ActivityScenario.launch(MainActivity::class.java).use { EspressoUtils.onActionItemClick(R.id.activity_action_settings, R.string.settings) - EspressoUtils.clickSetting(null, R.string.app) - EspressoUtils.clickSetting(null, R.string.developer_options) - EspressoUtils.clickSetting(null, R.string.git_repository_type) + EspressoUtils.clickSetting(R.string.app) + EspressoUtils.clickSetting(R.string.developer_options) + EspressoUtils.clickSetting(R.string.git_repository_type) pressBack() pressBack() - EspressoUtils.clickSetting(null, R.string.sync) - EspressoUtils.clickSetting(null, R.string.ssh_keygen_preference_title) + EspressoUtils.clickSetting(R.string.sync) + EspressoUtils.clickSetting(R.string.ssh_keygen_preference_title) onView(withText(param.keyType)).perform(click()) onView(withText(R.string.ssh_keygen_generate)).perform(click()) getInstrumentation().waitForIdleSync() diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/SyncingTest.java b/app/src/androidTest/java/com/orgzly/android/espresso/SyncingTest.java index d82bdf1ec..8a6d65fe3 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/SyncingTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/SyncingTest.java @@ -106,10 +106,10 @@ public void testAutoSyncIsTriggeredAfterCreatingNote() { // Set preference onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_sync", R.string.sync); - clickSetting("prefs_screen_auto_sync", R.string.auto_sync); - clickSetting("pref_key_auto_sync", R.string.auto_sync); - clickSetting("pref_key_auto_sync_on_note_create", R.string.pref_title_sync_after_note_create); + clickSetting(R.string.sync); + clickSetting(R.string.auto_sync); + clickSetting(R.string.auto_sync); + clickSetting(R.string.pref_title_sync_after_note_create); pressBack(); pressBack(); pressBack(); @@ -678,8 +678,8 @@ public void testSettingLinkToRenamedRepo() throws JSONException { /* Rename repository. */ onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_sync", R.string.sync); - clickSetting("pref_key_repos", R.string.repos_preference_title); + clickSetting(R.string.sync); + clickSetting(R.string.repos_preference_title); onListItem(0).perform(click()); onView(withId(R.id.activity_repo_dropbox_directory)).perform(replaceTextCloseKeyboard("repo-b")); onView(withId(R.id.fab)).perform(click()); // Repo done @@ -730,8 +730,8 @@ public void testRenamingReposRemovesLinksWhatUsedThem() throws JSONException { /* Rename all repositories. */ onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_sync", R.string.sync); - clickSetting("pref_key_repos", R.string.repos_preference_title); + clickSetting(R.string.sync); + clickSetting(R.string.repos_preference_title); onListItem(0).perform(click()); onView(withId(R.id.activity_repo_dropbox_directory)).perform(replaceTextCloseKeyboard("repo-1")); onView(withId(R.id.fab)).perform(click()); // Repo done diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/util/EspressoUtils.java b/app/src/androidTest/java/com/orgzly/android/espresso/util/EspressoUtils.java index cc401eaba..e660e63f6 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/util/EspressoUtils.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/util/EspressoUtils.java @@ -306,7 +306,7 @@ public static void onActionItemClick(int id, int resourceId) { } } - public static void clickSetting(String key, int title) { + public static void clickSetting(int title) { onView(withId(R.id.recycler_view)) .perform(RecyclerViewActions.actionOnItem( hasDescendant(withText(title)), click())); @@ -323,8 +323,8 @@ public static void settingsSetDoneKeywords(String keywords) { private static void settingsSetKeywords(int viewId, String keywords) { onActionItemClick(R.id.activity_action_settings, R.string.settings); - clickSetting("prefs_screen_notebooks", R.string.pref_title_notebooks); - clickSetting("pref_key_states", R.string.states); + clickSetting(R.string.pref_title_notebooks); + clickSetting(R.string.states); onView(withId(viewId)).perform(replaceTextCloseKeyboard(keywords)); onView(withText(android.R.string.ok)).perform(click()); diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/util/ScreenshotsTakingNotATest.kt b/app/src/androidTest/java/com/orgzly/android/espresso/util/ScreenshotsTakingNotATest.kt index 7adc581f4..81ddf0f4e 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/util/ScreenshotsTakingNotATest.kt +++ b/app/src/androidTest/java/com/orgzly/android/espresso/util/ScreenshotsTakingNotATest.kt @@ -169,8 +169,8 @@ class ScreenshotsTakingNotATest : OrgzlyTest() { fun settings() { startActivity(SettingsActivity::class.java) - clickSetting("", R.string.sync) - clickSetting("", R.string.repositories) + clickSetting(R.string.sync) + clickSetting(R.string.repositories) takeScreenshot("repos.png") } From 2dbeda852d512410a6d63bb5ad4d888d634c4653 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Tue, 16 Jul 2024 01:44:04 +0200 Subject: [PATCH 05/18] Add subfolder support in GitRepo - Loading from subdirectories works - Updating existing book in subdirectory works - Renaming across subdirectories works --- .../com/orgzly/android/data/DataRepository.kt | 8 +++---- .../android/git/GitFileSynchronizer.java | 22 ++++++++++++++----- .../com/orgzly/android/repos/GitRepo.java | 10 ++++----- .../orgzly/android/repos/TwoWaySyncRepo.kt | 2 ++ .../java/com/orgzly/android/sync/SyncUtils.kt | 18 +++++++-------- 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/orgzly/android/data/DataRepository.kt b/app/src/main/java/com/orgzly/android/data/DataRepository.kt index 5127448cc..40027bcb9 100644 --- a/app/src/main/java/com/orgzly/android/data/DataRepository.kt +++ b/app/src/main/java/com/orgzly/android/data/DataRepository.kt @@ -103,7 +103,7 @@ class DataRepository @Inject constructor( val book = getBookView(bookId) ?: throw IOException(resources.getString(R.string.book_does_not_exist_anymore)) - val fileName: String = BookName.getFileName(context, book) + val repositoryPath: String = BookName.getFileName(context, book) try { /* Prefer link. */ @@ -113,7 +113,7 @@ class DataRepository @Inject constructor( BookAction.Type.PROGRESS, resources.getString(R.string.force_saving_to_uri, repoEntity))) - saveBookToRepo(repoEntity, fileName, book, BookFormat.ORG) + saveBookToRepo(repoEntity, repositoryPath, book, BookFormat.ORG) val savedBook = getBookView(bookId) @@ -144,7 +144,7 @@ class DataRepository @Inject constructor( @Throws(IOException::class) fun saveBookToRepo( repoEntity: Repo, - fileName: String, + repositoryPath: String, bookView: BookView, @Suppress("UNUSED_PARAMETER") format: BookFormat) { @@ -158,7 +158,7 @@ class DataRepository @Inject constructor( NotesOrgExporter(this).exportBook(bookView.book, tmpFile) /* Upload to repo. */ - uploadedBook = repo.storeBook(tmpFile, fileName) + uploadedBook = repo.storeBook(tmpFile, repositoryPath) } finally { /* Delete temporary file. */ diff --git a/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java b/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java index cfa0fa4fe..21c340a60 100644 --- a/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java +++ b/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java @@ -396,17 +396,28 @@ public void addAndCommitNewFile(File sourceFile, String repositoryPath) throws I if (destinationFile.exists()) { throw new IOException("Can't add new file " + repositoryPath + " that already exists."); } + ensureDirectoryHierarchy(repositoryPath); updateAndCommitFile(sourceFile, repositoryPath); } + private void ensureDirectoryHierarchy(String repositoryPath) throws IOException { + if (repositoryPath.contains("/")) { + File targetDir = repoDirectoryFile(repositoryPath).getParentFile(); + if (!(targetDir.exists() || targetDir.mkdirs())) { + throw new IOException("The directory " + targetDir.getAbsolutePath() + " could " + + "not be created"); + } + } + } + private void updateAndCommitFile( - File sourceFile, String repositoryPath) throws IOException { - File destinationFile = repoDirectoryFile(repositoryPath); + File sourceFile, String fileName) throws IOException { + File destinationFile = repoDirectoryFile(fileName); MiscUtils.copyFile(sourceFile, destinationFile); try { - git.add().addFilepattern(repositoryPath).call(); + git.add().addFilepattern(fileName).call(); if (!gitRepoIsClean()) - commit(String.format("Orgzly update: %s", repositoryPath)); + commit(String.format("Orgzly update: %s", fileName)); } catch (GitAPIException e) { throw new IOException("Failed to commit changes."); } @@ -490,8 +501,9 @@ public boolean renameFileInRepo(String oldFileName, String newFileName) throws I File newFile = repoDirectoryFile(newFileName); // Abort if destination file exists if (newFile.exists()) { - throw new IOException("Can't add new file " + newFileName + " that already exists."); + throw new IOException("Repository file " + newFileName + " already exists."); } + ensureDirectoryHierarchy(newFileName); // Copy the file contents and add it to the index MiscUtils.copyFile(oldFile, newFile); try { diff --git a/app/src/main/java/com/orgzly/android/repos/GitRepo.java b/app/src/main/java/com/orgzly/android/repos/GitRepo.java index 5cd0b585b..1c8c10ddf 100644 --- a/app/src/main/java/com/orgzly/android/repos/GitRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/GitRepo.java @@ -185,16 +185,16 @@ public boolean isAutoSyncSupported() { return true; } - public VersionedRook storeBook(File file, String fileName) throws IOException { - File destination = synchronizer.repoDirectoryFile(fileName); + public VersionedRook storeBook(File file, String repositoryPath) throws IOException { + File destination = synchronizer.repoDirectoryFile(repositoryPath); if (destination.exists()) { - synchronizer.updateAndCommitExistingFile(file, fileName); + synchronizer.updateAndCommitExistingFile(file, repositoryPath); } else { - synchronizer.addAndCommitNewFile(file, fileName); + synchronizer.addAndCommitNewFile(file, repositoryPath); } synchronizer.tryPush(); - return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(fileName).build()); + return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(repositoryPath).build()); } private RevWalk walk() { diff --git a/app/src/main/java/com/orgzly/android/repos/TwoWaySyncRepo.kt b/app/src/main/java/com/orgzly/android/repos/TwoWaySyncRepo.kt index a63aab93e..d3d7dfa7e 100644 --- a/app/src/main/java/com/orgzly/android/repos/TwoWaySyncRepo.kt +++ b/app/src/main/java/com/orgzly/android/repos/TwoWaySyncRepo.kt @@ -9,4 +9,6 @@ interface TwoWaySyncRepo { fun syncBook(uri: Uri, current: VersionedRook?, fromDB: File): TwoWaySyncResult fun tryPushIfHeadDiffersFromRemote() + + fun getUri(): Uri } \ No newline at end of file diff --git a/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt b/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt index 6031f13f2..7ad61d2b1 100644 --- a/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt +++ b/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt @@ -89,7 +89,7 @@ object SyncUtils { fun syncNamesake(dataRepository: DataRepository, namesake: BookNamesake): BookAction { val repoEntity: Repo? val repoUrl: String - val fileName: String + val repositoryPath: String var bookAction: BookAction? = null // FIXME: This is a pretty nasty hack that completely circumvents the existing code path @@ -157,26 +157,26 @@ object SyncUtils { BookSyncStatus.ONLY_BOOK_WITHOUT_LINK_AND_ONE_REPO -> { repoEntity = dataRepository.getRepos().iterator().next() repoUrl = repoEntity.url - fileName = BookName.fileName(namesake.book.book.name, BookFormat.ORG) + repositoryPath = BookName.fileName(namesake.book.book.name, BookFormat.ORG) /* Set repo link before saving to ensure repo ignore rules are checked */ dataRepository.setLink(namesake.book.book.id, repoEntity) - dataRepository.saveBookToRepo(repoEntity, fileName, namesake.book, BookFormat.ORG) + dataRepository.saveBookToRepo(repoEntity, repositoryPath, namesake.book, BookFormat.ORG) bookAction = BookAction.forNow(BookAction.Type.INFO, namesake.status.msg(repoUrl)) } BookSyncStatus.BOOK_WITH_LINK_LOCAL_MODIFIED -> { repoEntity = namesake.book.linkRepo repoUrl = repoEntity!!.url - fileName = BookName.getFileName(repoUrl.toUri(), namesake.book.syncedTo!!.uri) - dataRepository.saveBookToRepo(repoEntity, fileName, namesake.book, BookFormat.ORG) + repositoryPath = BookName.getFileName(repoUrl.toUri(), namesake.book.syncedTo!!.uri) + dataRepository.saveBookToRepo(repoEntity, repositoryPath, namesake.book, BookFormat.ORG) bookAction = BookAction.forNow(BookAction.Type.INFO, namesake.status.msg(repoUrl)) } BookSyncStatus.ONLY_BOOK_WITH_LINK -> { repoEntity = namesake.book.linkRepo repoUrl = repoEntity!!.url - fileName = BookName.fileName(namesake.book.book.name, BookFormat.ORG) - dataRepository.saveBookToRepo(repoEntity, fileName, namesake.book, BookFormat.ORG) + repositoryPath = BookName.fileName(namesake.book.book.name, BookFormat.ORG) + dataRepository.saveBookToRepo(repoEntity, repositoryPath, namesake.book, BookFormat.ORG) bookAction = BookAction.forNow(BookAction.Type.INFO, namesake.status.msg(repoUrl)) } } @@ -192,7 +192,7 @@ object SyncUtils { var noNewMergeConflicts = true // If there are only local changes, the GitRepo.syncBook method is overly complicated. if (namesake.status == BookSyncStatus.BOOK_WITH_LINK_LOCAL_MODIFIED) { - val fileName = BookName.getFileName(App.getAppContext(), namesake.book.syncedTo!!.uri) + val fileName = BookName.getFileName(repo.getUri(), namesake.book.syncedTo!!.uri) dataRepository.saveBookToRepo(namesake.book.linkRepo!!, fileName, namesake.book, BookFormat.ORG) } else { val dbFile = dataRepository.getTempBookFile() @@ -204,7 +204,7 @@ object SyncUtils { newRook = newRook1 // We only need to write it if syncback is needed if (loadFile != null) { - val fileName = BookName.getFileName(App.getAppContext(), newRook.uri) + val fileName = BookName.getFileName(repo.getUri(), newRook.uri) val bookName = BookName.fromFileName(fileName) if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Loading from file '$loadFile'") dataRepository.loadBookFromFile( From 06de814c979b6ba04a0243d2a49dbe872ae5e4b9 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Wed, 24 Jul 2024 01:08:10 +0200 Subject: [PATCH 06/18] Support subfolders during force load and force save --- app/src/main/java/com/orgzly/android/BookName.java | 5 +++-- app/src/main/java/com/orgzly/android/data/DataRepository.kt | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/orgzly/android/BookName.java b/app/src/main/java/com/orgzly/android/BookName.java index aeee48304..87c29c3eb 100644 --- a/app/src/main/java/com/orgzly/android/BookName.java +++ b/app/src/main/java/com/orgzly/android/BookName.java @@ -5,6 +5,7 @@ import androidx.documentfile.provider.DocumentFile; import com.orgzly.BuildConfig; +import com.orgzly.android.db.entity.BookView; import com.orgzly.android.repos.Rook; import com.orgzly.android.util.LogUtils; @@ -31,9 +32,9 @@ private BookName(String fileName, String name, BookFormat format) { mFormat = format; } - public static String getFileName(Context context, com.orgzly.android.db.entity.BookView bookView) { + public static String getFileName(BookView bookView) { if (bookView.getSyncedTo() != null) { - return getFileName(context, bookView.getSyncedTo().getUri()); + return getFileName(bookView.getSyncedTo().getRepoUri(), bookView.getSyncedTo().getUri()); } else { return fileName(bookView.getBook().getName(), BookFormat.ORG); diff --git a/app/src/main/java/com/orgzly/android/data/DataRepository.kt b/app/src/main/java/com/orgzly/android/data/DataRepository.kt index 40027bcb9..0861b2007 100644 --- a/app/src/main/java/com/orgzly/android/data/DataRepository.kt +++ b/app/src/main/java/com/orgzly/android/data/DataRepository.kt @@ -80,7 +80,7 @@ class DataRepository @Inject constructor( BookAction.Type.PROGRESS, resources.getString(R.string.force_loading_from_uri, book.linkRepo.url))) - val fileName = BookName.getFileName(context, book) + val fileName = BookName.getFileName(book) val loadedBook = loadBookFromRepo(book.linkRepo.id, book.linkRepo.type, book.linkRepo.url, fileName) @@ -103,7 +103,7 @@ class DataRepository @Inject constructor( val book = getBookView(bookId) ?: throw IOException(resources.getString(R.string.book_does_not_exist_anymore)) - val repositoryPath: String = BookName.getFileName(context, book) + val repositoryPath: String = BookName.getFileName(book) try { /* Prefer link. */ From 9abd9174988ca27e4268d32fbbfaf8165af565d3 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Sat, 24 Aug 2024 00:15:21 +0200 Subject: [PATCH 07/18] Add subfolder support in WebdavRepo, DropboxRepo Also, ensure that storeBook and retrieveBook return the same rook URI. --- .../orgzly/android/repos/DropboxClient.java | 99 +++++++++++++------ .../com/orgzly/android/repos/DropboxRepo.java | 8 ++ .../com/orgzly/android/repos/WebdavRepo.kt | 82 ++++++++++++--- 3 files changed, 145 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/orgzly/android/repos/DropboxClient.java b/app/src/main/java/com/orgzly/android/repos/DropboxClient.java index 940039415..fb80c6ef6 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxClient.java +++ b/app/src/main/java/com/orgzly/android/repos/DropboxClient.java @@ -145,41 +145,56 @@ public List getBooks(Uri repoUri, RepoIgnoreNode ignores) throws /* Strip trailing slashes. */ path = path.replaceAll("/+$", ""); + List folderPaths = new ArrayList<>(List.of(path)); + try { if (ROOT_PATH.equals(path) || dbxClient.files().getMetadata(path) instanceof FolderMetadata) { /* Get folder content. */ - ListFolderResult result = dbxClient.files().listFolder(path); - while (true) { - for (Metadata metadata : result.getEntries()) { - if (metadata instanceof FileMetadata) { - FileMetadata file = (FileMetadata) metadata; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (ignores.isPathIgnored(file.getName(), false)) { - continue; + while (folderPaths.size() > 0) { + ListFolderResult result = dbxClient.files().listFolder(folderPaths.remove(0)); + while (true) { + for (Metadata metadata : result.getEntries()) { + String pathRelativeToRepoRoot = + metadata.getPathDisplay().replaceAll("^" + path + "/", ""); + if (metadata instanceof FileMetadata) { + FileMetadata file = (FileMetadata) metadata; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (ignores.isPathIgnored(pathRelativeToRepoRoot, false)) { + continue; + } } - } - if (BookName.isSupportedFormatFileName(file.getName())) { - Uri uri = repoUri.buildUpon().appendPath(file.getName()).build(); - VersionedRook book = new VersionedRook( - repoId, - RepoType.DROPBOX, - repoUri, - uri, - file.getRev(), - file.getServerModified().getTime()); - - list.add(book); + if (BookName.isSupportedFormatFileName(file.getName())) { + String encodedRelativePath = Uri.encode(pathRelativeToRepoRoot, "/"); + Uri uri = repoUri.buildUpon().appendEncodedPath(encodedRelativePath).build(); + VersionedRook book = new VersionedRook( + repoId, + RepoType.DROPBOX, + repoUri, + uri, + file.getRev(), + file.getServerModified().getTime()); + + list.add(book); + } + } + if (metadata instanceof FolderMetadata) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (ignores.isPathIgnored(pathRelativeToRepoRoot, true)) { + continue; + } + } + folderPaths.add(metadata.getPathDisplay()); } } - } - if (!result.getHasMore()) { - break; - } + if (!result.getHasMore()) { + break; + } - result = dbxClient.files().listFolderContinue(result.getCursor()); + result = dbxClient.files().listFolderContinue(result.getCursor()); + } } } else { @@ -204,13 +219,18 @@ public List getBooks(Uri repoUri, RepoIgnoreNode ignores) throws return list; } + private Uri getFullUriFromRelativePath(Uri repoUri, String relativePath) { + String encodedFileName = Uri.encode(relativePath, "/"); + return Uri.withAppendedPath(repoUri, encodedFileName); + } + /** * Download file from Dropbox and store it to a local file. */ - public VersionedRook download(Uri repoUri, String fileName, File localFile) throws IOException { + public VersionedRook download(Uri repoUri, String relativePath, File localFile) throws IOException { linkedOrThrow(); - Uri uri = repoUri.buildUpon().appendPath(fileName).build(); + Uri uri = getFullUriFromRelativePath(repoUri, relativePath); OutputStream out = new BufferedOutputStream(new FileOutputStream(localFile)); @@ -267,10 +287,10 @@ public InputStream streamFile(Uri repoUri, String fileName) throws IOException { } /** Upload file to Dropbox. */ - public VersionedRook upload(File file, Uri repoUri, String fileName) throws IOException { + public VersionedRook upload(File file, Uri repoUri, String relativePath) throws IOException { linkedOrThrow(); - Uri bookUri = repoUri.buildUpon().appendPath(fileName).build(); + Uri bookUri = getFullUriFromRelativePath(repoUri, relativePath); if (file.length() > UPLOAD_FILE_SIZE_LIMIT * 1024 * 1024) { throw new IOException(LARGE_FILE); @@ -348,4 +368,23 @@ public VersionedRook move(Uri repoUri, Uri from, Uri to) throws IOException { } } } + + public void deleteFolder(String path) throws IOException { + linkedOrThrow(); + + try { + if (dbxClient.files().getMetadata(path) instanceof FolderMetadata) { + dbxClient.files().deleteV2(path); + } else { + throw new IOException("Not a directory: " + path); + } + } catch (DbxException e) { + e.printStackTrace(); + if (e.getMessage() != null) { + throw new IOException("Failed deleting " + path + " on Dropbox: " + e.getMessage()); + } else { + throw new IOException("Failed deleting " + path + " on Dropbox: " + e); + } + } + } } diff --git a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java index 74dd25a65..e25acec63 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java @@ -69,6 +69,14 @@ public void delete(Uri uri) throws IOException { client.delete(uri.getPath()); } + /** + * Intended for tests. The delete() method does not allow deleting directories. + * @param uri + */ + public void deleteDirectory(Uri uri) throws IOException { + client.deleteFolder(uri.getPath()); + } + @Override public String toString() { return repoUri.toString(); diff --git a/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt b/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt index da62353a2..a1be5f5ba 100644 --- a/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt +++ b/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt @@ -10,7 +10,6 @@ import com.burgstaller.okhttp.digest.CachingAuthenticator import com.burgstaller.okhttp.digest.Credentials import com.burgstaller.okhttp.digest.DigestAuthenticator import com.orgzly.android.BookName -import com.orgzly.android.util.UriUtils import com.thegrizzlylabs.sardineandroid.DavResource import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine import okhttp3.OkHttpClient @@ -18,10 +17,12 @@ import okio.Buffer import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream +import java.io.IOException import java.io.InputStream +import java.net.URI import java.security.KeyStore import java.security.cert.CertificateFactory -import java.util.* +import java.util.Arrays import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit import javax.net.ssl.SSLContext @@ -166,16 +167,16 @@ class WebdavRepo( val ignores = RepoIgnoreNode(this) return sardine - .list(url) + .list(url, -1) .mapNotNull { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (it.isDirectory || !BookName.isSupportedFormatFileName(it.name) || ignores.isPathIgnored(it.name, false)) { + if (!BookName.isSupportedFormatFileName(it.name) || ignores.isPathIgnored(it.getRelativePath(), it.isDirectory)) { null } else { it.toVersionedRook() } } else { - if (it.isDirectory || !BookName.isSupportedFormatFileName(it.name)) { + if (!BookName.isSupportedFormatFileName(it.name)) { null } else { it.toVersionedRook() @@ -204,18 +205,50 @@ class WebdavRepo( return sardine.get(fileUrl) } - override fun storeBook(file: File?, fileName: String?): VersionedRook { - val fileUrl = Uri.withAppendedPath(uri, fileName).toUrl() + private fun ensureDirectoryHierarchy(relativePath: String) { + val levels: ArrayList = ArrayList(relativePath.split("/")) + // N.B. Strip off trailing slash from repo URL, if present + var currentDir: String = uri.toString().replace(Regex("/$"), "") + while (levels.size > 1) { + val nextDirName: String = levels.removeAt(0) + currentDir = "$currentDir/$nextDirName" + if (!sardine.exists(currentDir)) { + sardine.createDirectory(currentDir) + } + } + } + + override fun storeBook(file: File, fileName: String): VersionedRook { + val encodedFileName = Uri.encode(fileName, "/") + if (encodedFileName != null) { + if (encodedFileName.contains("/")) { + ensureDirectoryHierarchy(encodedFileName) + } + } + val fileUrl = uri.buildUpon().appendEncodedPath(encodedFileName).build().toUrl() sardine.put(fileUrl, file, null) return sardine.list(fileUrl).first().toVersionedRook() } - override fun renameBook(from: Uri, name: String?): VersionedRook { - val destUrl = UriUtils.getUriForNewName(from, name).toUrl() - sardine.move(from.toUrl(), destUrl) - return sardine.list(destUrl).first().toVersionedRook() + override fun renameBook(oldFullUri: Uri, newName: String): VersionedRook { + val oldBookName = BookName.fromFileName(BookName.getFileName(uri, oldFullUri)) + val newRelativePath = BookName.fileName(newName, oldBookName.format) + val newEncodedRelativePath = Uri.encode(newRelativePath, "/") + val newFullUrl = uri.buildUpon().appendEncodedPath(newEncodedRelativePath).build().toUrl() + + /* Abort if destination file already exists. */ + if (sardine.exists(newFullUrl)) { + throw IOException("File at $newFullUrl already exists") + } + + if (newName.contains("/")) { + ensureDirectoryHierarchy(newEncodedRelativePath) + } + + sardine.move(oldFullUri.toUrl(), newFullUrl) + return sardine.list(newFullUrl).first().toVersionedRook() } override fun delete(uri: Uri) { @@ -227,13 +260,34 @@ class WebdavRepo( repoId, RepoType.WEBDAV, uri, - Uri.withAppendedPath(uri, this.name), - this.name + this.modified.time.toString(), + Uri.parse(this.getFullUrlString()), + this.modified.time.toString(), this.modified.time ) } + /** + * A WebDAV href can be either an absolute (full) URI, or an "absolute path" + * (cf. http://www.webdav.org/specs/rfc4918.html#url-handling). The full URI can be built from + * the absolute path. + */ + private fun DavResource.getFullUrlString(): String { + if (this.href.isAbsolute) { + // absolute-URI - return the href as-is + return this.href.toString() + } else { + // path-absolute - build the absolut URI + return uri.scheme + "://" + uri.authority + this.href.toString() + } + } + + private fun DavResource.getRelativePath(): String { + val absoluteUri = URI.create(this.getFullUrlString()) + val relativePath = URI.create(uri.toString()).relativize(absoluteUri) + return relativePath.path + } + private fun Uri.toUrl(): String { return this.toString().replace("^(?:web)?dav(s?://)".toRegex(), "http$1") } -} +} \ No newline at end of file From 720ca7dcf4a7c98d13c362acab601752cbc628e6 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Thu, 25 Jul 2024 14:13:07 +0200 Subject: [PATCH 08/18] Improve ContentRepo.storeBook() after tests on API 29 The DocumentFile.exists() method always returned true for not-yet-created subdirectories (cf. https://stackoverflow.com/q/51661385). Also, rename the "fileName" argument back to "path", since it is now actually a path name, not a file name. (This should be done in more places, eventually.) --- .../com/orgzly/android/repos/ContentRepo.java | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/orgzly/android/repos/ContentRepo.java b/app/src/main/java/com/orgzly/android/repos/ContentRepo.java index f56da3bf8..d49246bad 100644 --- a/app/src/main/java/com/orgzly/android/repos/ContentRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/ContentRepo.java @@ -170,27 +170,25 @@ public InputStream openRepoFileInputStream(String fileName) throws IOException { } @Override - public VersionedRook storeBook(File file, String fileName) throws IOException { + public VersionedRook storeBook(File file, String path) throws IOException { if (!file.exists()) { throw new FileNotFoundException("File " + file + " does not exist"); } - DocumentFile destinationFile = getDocumentFileFromFileName(fileName); - if (!destinationFile.exists()) { - if (fileName.contains("/")) { - DocumentFile destinationDir = ensureDirectoryHierarchy(fileName); - destinationFile = destinationDir.createFile("text/*", Uri.parse(fileName).getLastPathSegment()); - } else { - repoDocumentFile.createFile("text/*", fileName); + DocumentFile destinationFile = getDocumentFileFromFileName(path); + if (path.contains("/")) { + DocumentFile destinationDir = ensureDirectoryHierarchy(path); + String fileName = Uri.parse(path).getLastPathSegment(); + if (destinationDir.findFile(fileName) == null) { + destinationFile = destinationDir.createFile("text/*", fileName); + } + } else { + if (!destinationFile.exists()) { + repoDocumentFile.createFile("text/*", path); } } - OutputStream out = context.getContentResolver().openOutputStream(destinationFile.getUri()); - try { + try (OutputStream out = context.getContentResolver().openOutputStream(destinationFile.getUri())) { MiscUtils.writeFileToStream(file, out); - } finally { - if (out != null) { - out.close(); - } } long mtime = destinationFile.lastModified(); From 44fbc33410b0336554e2b2b3f210f80bd5c9c310 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Sat, 27 Jul 2024 10:52:16 +0200 Subject: [PATCH 09/18] Rename the arguments of SyncRepo.renameBook() --- .../com/orgzly/android/repos/ContentRepo.java | 15 +++++++-------- .../com/orgzly/android/repos/DatabaseRepo.java | 6 +++--- .../com/orgzly/android/repos/DirectoryRepo.java | 8 ++++---- .../com/orgzly/android/repos/DropboxRepo.java | 6 +++--- .../java/com/orgzly/android/repos/GitRepo.java | 6 +++--- .../java/com/orgzly/android/repos/MockRepo.java | 4 ++-- .../java/com/orgzly/android/repos/SyncRepo.java | 2 +- 7 files changed, 23 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/orgzly/android/repos/ContentRepo.java b/app/src/main/java/com/orgzly/android/repos/ContentRepo.java index d49246bad..54781ab9f 100644 --- a/app/src/main/java/com/orgzly/android/repos/ContentRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/ContentRepo.java @@ -222,28 +222,28 @@ private DocumentFile ensureDirectoryHierarchy(String relativePath) { * Allows renaming a notebook to any subdirectory (indicated with a "/"), ensuring that all * required subdirectories are created, if they do not already exist. Note that the file is * moved, but no "abandoned" directories are deleted. - * @param oldUri + * @param oldFullUri * @param newName * @return * @throws IOException */ @Override - public VersionedRook renameBook(Uri oldUri, String newName) throws IOException { - DocumentFile oldDocFile = DocumentFile.fromSingleUri(context, oldUri); + public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException { + DocumentFile oldDocFile = DocumentFile.fromSingleUri(context, oldFullUri); long mtime = oldDocFile.lastModified(); String rev = String.valueOf(mtime); String oldDocFileName = oldDocFile.getName(); Uri oldDirUri = Uri.parse( - oldUri.toString().replace( + oldFullUri.toString().replace( Uri.encode("/" + oldDocFile.getName()), "" ) ); - BookName oldBookName = BookName.fromFileName(BookName.getFileName(repoUri, oldUri)); + BookName oldBookName = BookName.fromFileName(BookName.getFileName(repoUri, oldFullUri)); String newRelativePath = BookName.fileName(newName, oldBookName.getFormat()); String newDocFileName = Uri.parse(newRelativePath).getLastPathSegment(); DocumentFile newDir; - Uri newUri = oldUri; + Uri newUri = oldFullUri; if (newName.contains("/")) { newDir = ensureDirectoryHierarchy(newName); @@ -261,8 +261,7 @@ public VersionedRook renameBook(Uri oldUri, String newName) throws IOException { // File should be moved to a different directory if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { newUri = DocumentsContract.moveDocument( - context.getContentResolver(), - oldUri, + context.getContentResolver(), oldFullUri, oldDirUri, newDir.getUri() ); diff --git a/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java b/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java index 7d371a12e..10b81b958 100644 --- a/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java @@ -73,9 +73,9 @@ public VersionedRook storeBook(File file, String fileName) throws IOException { } @Override - public VersionedRook renameBook(Uri fromUri, String name) { - Uri toUri = UriUtils.getUriForNewName(fromUri, name); - return dbRepo.renameBook(repoId, fromUri, toUri); + public VersionedRook renameBook(Uri oldFullUri, String newName) { + Uri toUri = UriUtils.getUriForNewName(oldFullUri, newName); + return dbRepo.renameBook(repoId, oldFullUri, toUri); } @Override diff --git a/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java b/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java index db2b59064..c2be91cbc 100644 --- a/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java @@ -178,15 +178,15 @@ public VersionedRook storeBook(File file, String fileName) throws IOException { } @Override - public VersionedRook renameBook(Uri fromUri, String name) throws IOException { - String fromFilePath = fromUri.getPath(); + public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException { + String fromFilePath = oldFullUri.getPath(); if (fromFilePath == null) { - throw new IllegalArgumentException("No path in " + fromUri); + throw new IllegalArgumentException("No path in " + oldFullUri); } File fromFile = new File(fromFilePath); - Uri newUri = UriUtils.getUriForNewName(fromUri, name); + Uri newUri = UriUtils.getUriForNewName(oldFullUri, newName); String toFilePath = newUri.getPath(); if (toFilePath == null) { diff --git a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java index e25acec63..f3006f674 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java @@ -59,9 +59,9 @@ public VersionedRook storeBook(File file, String fileName) throws IOException { } @Override - public VersionedRook renameBook(Uri fromUri, String name) throws IOException { - Uri toUri = UriUtils.getUriForNewName(fromUri, name); - return client.move(repoUri, fromUri, toUri); + public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException { + Uri toUri = UriUtils.getUriForNewName(oldFullUri, newName); + return client.move(repoUri, oldFullUri, toUri); } @Override diff --git a/app/src/main/java/com/orgzly/android/repos/GitRepo.java b/app/src/main/java/com/orgzly/android/repos/GitRepo.java index 1c8c10ddf..1e0b6bddf 100644 --- a/app/src/main/java/com/orgzly/android/repos/GitRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/GitRepo.java @@ -297,9 +297,9 @@ public void delete(Uri uri) throws IOException { if (synchronizer.deleteFileFromRepo(uri)) synchronizer.tryPush(); } - public VersionedRook renameBook(Uri oldUri, String newBookName) throws IOException { - String oldFileName = oldUri.toString().replaceFirst("^/", ""); - String newFileName = BookName.fileName(newBookName, BookFormat.ORG); + public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException { + String oldFileName = oldFullUri.toString().replaceFirst("^/", ""); + String newFileName = BookName.fileName(newName, BookFormat.ORG); if (synchronizer.renameFileInRepo(oldFileName, newFileName)) { synchronizer.tryPush(); return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(newFileName).build()); diff --git a/app/src/main/java/com/orgzly/android/repos/MockRepo.java b/app/src/main/java/com/orgzly/android/repos/MockRepo.java index 9145c9a3d..35f8b9cea 100644 --- a/app/src/main/java/com/orgzly/android/repos/MockRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/MockRepo.java @@ -70,9 +70,9 @@ public VersionedRook storeBook(File file, String fileName) throws IOException { } @Override - public VersionedRook renameBook(Uri fromUri, String name) throws IOException { + public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException { SystemClock.sleep(SLEEP_FOR_STORE_BOOK); - return databaseRepo.renameBook(fromUri, name); + return databaseRepo.renameBook(oldFullUri, newName); } @Override diff --git a/app/src/main/java/com/orgzly/android/repos/SyncRepo.java b/app/src/main/java/com/orgzly/android/repos/SyncRepo.java index ed22ef2d1..6009051a6 100644 --- a/app/src/main/java/com/orgzly/android/repos/SyncRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/SyncRepo.java @@ -48,7 +48,7 @@ public interface SyncRepo { */ VersionedRook storeBook(File file, String fileName) throws IOException; - VersionedRook renameBook(Uri from, String name) throws IOException; + VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException; // VersionedRook moveBook(Uri from, Uri uri) throws IOException; From bfd7c3b4dbe850357bf5bbdd4b7d690713f2b3f4 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Sat, 24 Aug 2024 00:22:02 +0200 Subject: [PATCH 10/18] Fix two bugs in DropboxRepo.renameBook() - Ensure URIs are correctly encoded - Gracefully handle conflict with already existing file --- .../com/orgzly/android/repos/DropboxClient.java | 6 ++++++ .../java/com/orgzly/android/repos/DropboxRepo.java | 14 +++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/orgzly/android/repos/DropboxClient.java b/app/src/main/java/com/orgzly/android/repos/DropboxClient.java index fb80c6ef6..745384b02 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxClient.java +++ b/app/src/main/java/com/orgzly/android/repos/DropboxClient.java @@ -343,6 +343,12 @@ public void delete(String path) throws IOException { public VersionedRook move(Uri repoUri, Uri from, Uri to) throws IOException { linkedOrThrow(); + /* Abort if destination file already exists. */ + try { + if (dbxClient.files().getMetadata(to.getPath()) instanceof FileMetadata) + throw new IOException("File at " + to.getPath() + " already exists"); + } catch (DbxException ignored) {} + try { RelocationResult relocationRes = dbxClient.files().moveV2(from.getPath(), to.getPath()); Metadata metadata = relocationRes.getMetadata(); diff --git a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java index f3006f674..1dbe611ab 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java @@ -3,11 +3,12 @@ import android.content.Context; import android.net.Uri; -import com.orgzly.android.util.UriUtils; +import androidx.annotation.NonNull; + +import com.orgzly.android.BookName; import java.io.File; import java.io.IOException; - import java.io.InputStream; import java.util.List; @@ -60,8 +61,11 @@ public VersionedRook storeBook(File file, String fileName) throws IOException { @Override public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException { - Uri toUri = UriUtils.getUriForNewName(oldFullUri, newName); - return client.move(repoUri, oldFullUri, toUri); + BookName oldBookName = BookName.fromFileName(BookName.getFileName(repoUri, oldFullUri)); + String newRelativePath = BookName.fileName(newName, oldBookName.getFormat()); + String newEncodedRelativePath = Uri.encode(newRelativePath, "/"); + Uri newFullUri = repoUri.buildUpon().appendEncodedPath(newEncodedRelativePath).build(); + return client.move(repoUri, oldFullUri, newFullUri); } @Override @@ -71,12 +75,12 @@ public void delete(Uri uri) throws IOException { /** * Intended for tests. The delete() method does not allow deleting directories. - * @param uri */ public void deleteDirectory(Uri uri) throws IOException { client.deleteFolder(uri.getPath()); } + @NonNull @Override public String toString() { return repoUri.toString(); From 22d5f200d481a674860efa4e93158288b8f00740 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Sat, 24 Aug 2024 00:47:33 +0200 Subject: [PATCH 11/18] Rename ContentRepo to DocumentRepo Because the discrepancy between RepoType.DOCUMENT and ContentRepo is confusing, and "document repo" (from DocumentFile) seems more accurate than "content repo", since the latter presumably only stems from the URLs beginning with "content://". --- .../android/repos/{ContentRepo.java => DocumentRepo.java} | 6 +++--- app/src/main/java/com/orgzly/android/repos/RepoFactory.kt | 6 ++++-- .../android/ui/repo/directory/DirectoryRepoActivity.kt | 7 ++----- 3 files changed, 9 insertions(+), 10 deletions(-) rename app/src/main/java/com/orgzly/android/repos/{ContentRepo.java => DocumentRepo.java} (98%) diff --git a/app/src/main/java/com/orgzly/android/repos/ContentRepo.java b/app/src/main/java/com/orgzly/android/repos/DocumentRepo.java similarity index 98% rename from app/src/main/java/com/orgzly/android/repos/ContentRepo.java rename to app/src/main/java/com/orgzly/android/repos/DocumentRepo.java index 54781ab9f..1062d1bd7 100644 --- a/app/src/main/java/com/orgzly/android/repos/ContentRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DocumentRepo.java @@ -28,8 +28,8 @@ /** * Using DocumentFile, for devices running Lollipop or later. */ -public class ContentRepo implements SyncRepo { - private static final String TAG = ContentRepo.class.getName(); +public class DocumentRepo implements SyncRepo { + private static final String TAG = DocumentRepo.class.getName(); public static final String SCHEME = "content"; @@ -40,7 +40,7 @@ public class ContentRepo implements SyncRepo { private final DocumentFile repoDocumentFile; - public ContentRepo(RepoWithProps repoWithProps, Context context) { + public DocumentRepo(RepoWithProps repoWithProps, Context context) { Repo repo = repoWithProps.getRepo(); this.repoId = repo.getId(); diff --git a/app/src/main/java/com/orgzly/android/repos/RepoFactory.kt b/app/src/main/java/com/orgzly/android/repos/RepoFactory.kt index 25ad16cc7..f72a23298 100644 --- a/app/src/main/java/com/orgzly/android/repos/RepoFactory.kt +++ b/app/src/main/java/com/orgzly/android/repos/RepoFactory.kt @@ -25,8 +25,10 @@ class RepoFactory @Inject constructor( type == RepoType.DIRECTORY.id -> DirectoryRepo(repoWithProps, false) - type == RepoType.DOCUMENT.id -> - ContentRepo(repoWithProps, context) + type == RepoType.DOCUMENT.id -> DocumentRepo( + repoWithProps, + context + ) type == RepoType.WEBDAV.id -> WebdavRepo.getInstance(repoWithProps) diff --git a/app/src/main/java/com/orgzly/android/ui/repo/directory/DirectoryRepoActivity.kt b/app/src/main/java/com/orgzly/android/ui/repo/directory/DirectoryRepoActivity.kt index d9f1d57a2..9c9460176 100644 --- a/app/src/main/java/com/orgzly/android/ui/repo/directory/DirectoryRepoActivity.kt +++ b/app/src/main/java/com/orgzly/android/ui/repo/directory/DirectoryRepoActivity.kt @@ -1,10 +1,8 @@ package com.orgzly.android.ui.repo.directory import android.app.Activity -import android.content.ActivityNotFoundException import android.content.Intent import android.net.Uri -import android.os.Build import android.os.Bundle import android.text.TextUtils import androidx.activity.result.contract.ActivityResultContracts @@ -13,11 +11,10 @@ import androidx.lifecycle.ViewModelProvider import com.orgzly.BuildConfig import com.orgzly.R import com.orgzly.android.App -import com.orgzly.android.repos.ContentRepo +import com.orgzly.android.repos.DocumentRepo import com.orgzly.android.repos.RepoFactory import com.orgzly.android.repos.RepoType import com.orgzly.android.ui.CommonActivity -import com.orgzly.android.ui.repo.BrowserActivity import com.orgzly.android.ui.repo.RepoViewModel import com.orgzly.android.ui.repo.RepoViewModelFactory import com.orgzly.android.ui.showSnackbar @@ -129,7 +126,7 @@ class DirectoryRepoActivity : CommonActivity() { } private fun persistPermissions(uri: Uri) { - if (ContentRepo.SCHEME == uri.scheme) { + if (DocumentRepo.SCHEME == uri.scheme) { grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION From a69eb51e2be363273de51a15a56dda5e9d051050 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Sat, 24 Aug 2024 00:59:50 +0200 Subject: [PATCH 12/18] No context is needed to get a BookName when we have a Rook --- .../com/orgzly/android/repos/DataRepositoryTest.java | 2 +- .../com/orgzly/android/repos/DirectoryRepoTest.java | 10 +++++----- .../java/com/orgzly/android/repos/DropboxRepoTest.java | 3 ++- .../java/com/orgzly/android/repos/LocalDbRepoTest.java | 4 ++-- .../java/com/orgzly/android/repos/SyncTest.java | 6 +++--- app/src/main/java/com/orgzly/android/BookName.java | 4 ++-- .../java/com/orgzly/android/sync/BookNamesake.java | 4 +--- app/src/main/java/com/orgzly/android/sync/SyncUtils.kt | 4 +--- 8 files changed, 17 insertions(+), 20 deletions(-) diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java b/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java index a98a42c11..d0546380c 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java @@ -70,7 +70,7 @@ public void testLoadRook() throws IOException { assertEquals("remote-book-1", book.getBook().getName()); assertEquals("/remote-book-1.org", book.getSyncedTo().getUri().getPath()); - assertEquals("remote-book-1", BookName.getInstance(context, book.getSyncedTo()).getName()); + assertEquals("remote-book-1", BookName.fromRook(book.getSyncedTo()).getName()); assertEquals("0abcdef", book.getSyncedTo().getRevision()); assertEquals(1400067156000L, book.getSyncedTo().getMtime()); } diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java b/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java index 60133d30c..f8d59232c 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java @@ -62,8 +62,8 @@ public void testStoringFile() throws IOException { List books = repo.getBooks(); assertEquals(1, books.size()); - assertEquals("booky", BookName.getInstance(context, books.get(0)).getName()); - assertEquals("booky.org", BookName.getInstance(context, books.get(0)).getFileName()); + assertEquals("booky", BookName.fromRook(books.get(0)).getName()); + assertEquals("booky.org", BookName.fromRook(books.get(0)).getFileName()); assertEquals(repoUriString, books.get(0).getRepoUri().toString()); assertEquals(repoUriString + "/booky.org", books.get(0).getUri().toString()); } @@ -79,8 +79,8 @@ public void testExtension() throws IOException { List books = repo.getBooks(); assertEquals(1, books.size()); - assertEquals("03", BookName.getInstance(context, books.get(0)).getName()); - assertEquals("03.org", BookName.getInstance(context, books.get(0)).getFileName()); + assertEquals("03", BookName.fromRook(books.get(0)).getName()); + assertEquals("03.org", BookName.fromRook(books.get(0)).getFileName()); assertEquals(13, books.get(0).getRepoId()); assertEquals(repoUriString, books.get(0).getRepoUri().toString()); assertEquals(repoUriString + "/03.org", books.get(0).getUri().toString()); @@ -103,7 +103,7 @@ public void testGetBooksRespectsIgnoreRules() throws IOException { List books = repo.getBooks(); assertEquals(1, books.size()); - assertEquals("file2", BookName.getInstance(context, books.get(0)).getName()); + assertEquals("file2", BookName.fromRook(books.get(0)).getName()); assertEquals(repoUriString + "/file2.org", books.get(0).getUri().toString()); } diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java b/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java index 38a9ba6e0..070e75c9a 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java @@ -137,7 +137,8 @@ public void testDropboxFileRename() throws IOException { assertEquals(1, repo.getBooks().size()); assertEquals(repo.getUri() + "/notebook-renamed.org", repo.getBooks().get(0).getUri().toString()); - assertEquals("notebook-renamed.org", BookName.getInstance(context, repo.getBooks().get(0)).getFileName()); + assertEquals("notebook-renamed.org", + BookName.fromRook(repo.getBooks().get(0)).getFileName()); } private void uploadFileToRepo(Uri repoUri, String fileName, String fileContents) throws IOException { diff --git a/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java b/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java index 7438e8955..dd54e3c02 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java @@ -38,7 +38,7 @@ public void testGetBooksFromAllRepos() throws IOException { VersionedRook vrook = books.get(0); - assertEquals("mock-book", BookName.getInstance(context, vrook).getName()); + assertEquals("mock-book", BookName.fromRook(vrook).getName()); assertEquals("mock://repo-a", vrook.getRepoUri().toString()); assertEquals("mock://repo-a/mock-book.org", vrook.getUri().toString()); assertEquals("rev1", vrook.getRevision()); @@ -67,7 +67,7 @@ public void testStoringBook() throws IOException { assertEquals(1, books.size()); VersionedRook vrook = books.get(0); - assertEquals("local-book-1", BookName.getInstance(context, vrook).getName()); + assertEquals("local-book-1", BookName.fromRook(vrook).getName()); assertEquals("mock://repo-a", vrook.getRepoUri().toString()); assertTrue(vrook.getMtime() >= now); } diff --git a/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java b/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java index a07da5217..ff0be9b26 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java @@ -387,7 +387,7 @@ public void testMockFileRename() throws IOException { vrooks = repo.getBooks(); assertEquals(1, vrooks.size()); - assertEquals("Booky", BookName.getInstance(context, vrooks.get(0)).getName()); + assertEquals("Booky", BookName.fromRook(vrooks.get(0)).getName()); long mtime = vrooks.get(0).getMtime(); String rev = vrooks.get(0).getRevision(); @@ -401,7 +401,7 @@ public void testMockFileRename() throws IOException { vrooks = repo.getBooks(); assertEquals(1, vrooks.size()); - assertEquals("BookyRenamed", BookName.getInstance(context, vrooks.get(0)).getName()); + assertEquals("BookyRenamed", BookName.fromRook(vrooks.get(0)).getName()); assertEquals("mock://repo-a/BookyRenamed.org", vrooks.get(0).getUri().toString()); assertTrue(mtime < vrooks.get(0).getMtime()); assertNotSame(rev, vrooks.get(0).getRevision()); @@ -431,7 +431,7 @@ public void testDirectoryFileRename() throws IOException { assertEquals(1, repo.getBooks().size()); assertEquals(repo.getUri() + "/notebook-renamed.org", repo.getBooks().get(0).getUri().toString()); - assertEquals("notebook-renamed.org", BookName.getInstance(context, repo.getBooks().get(0)).getFileName()); + assertEquals("notebook-renamed.org", BookName.fromRook(repo.getBooks().get(0)).getFileName()); LocalStorage.deleteRecursive(new File(repoDir)); } diff --git a/app/src/main/java/com/orgzly/android/BookName.java b/app/src/main/java/com/orgzly/android/BookName.java index 87c29c3eb..e2cf421af 100644 --- a/app/src/main/java/com/orgzly/android/BookName.java +++ b/app/src/main/java/com/orgzly/android/BookName.java @@ -78,8 +78,8 @@ public static String getFileName(Uri repoUri, Uri fileUri) { } } - public static BookName getInstance(Context context, Rook rook) { - return fromFileName(getFileName(context, rook.getUri())); + public static BookName fromRook(Rook rook) { + return fromFileName(getFileName(rook.getRepoUri(), rook.getUri())); } public static boolean isSupportedFormatFileName(String fileName) { diff --git a/app/src/main/java/com/orgzly/android/sync/BookNamesake.java b/app/src/main/java/com/orgzly/android/sync/BookNamesake.java index ef49d2ea7..eaa4390cc 100644 --- a/app/src/main/java/com/orgzly/android/sync/BookNamesake.java +++ b/app/src/main/java/com/orgzly/android/sync/BookNamesake.java @@ -1,7 +1,5 @@ package com.orgzly.android.sync; -import android.content.Context; - import com.orgzly.android.BookName; import com.orgzly.android.db.entity.BookAction; import com.orgzly.android.db.entity.BookView; @@ -37,7 +35,7 @@ public BookNamesake(String name) { /** * Create links between each local book and each remote book with the same name. */ - public static Map getAll(Context context, List books, List versionedRooks) { + public static Map getAll(List books, List versionedRooks) { Map namesakes = new HashMap<>(); /* Create links from all local books first. */ diff --git a/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt b/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt index 7ad61d2b1..f1081adc7 100644 --- a/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt +++ b/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt @@ -2,7 +2,6 @@ package com.orgzly.android.sync import androidx.core.net.toUri import com.orgzly.BuildConfig -import com.orgzly.android.App import com.orgzly.android.BookFormat import com.orgzly.android.BookName import com.orgzly.android.NotesOrgExporter @@ -64,8 +63,7 @@ object SyncUtils { val versionedRooks = getBooksFromAllRepos(dataRepository, repos) /* Group local and remote books by name. */ - val namesakes = BookNamesake.getAll( - App.getAppContext(), localBooks, versionedRooks) + val namesakes = BookNamesake.getAll(localBooks, versionedRooks) /* If there is no local book, create empty "dummy" one. */ for (namesake in namesakes.values) { From 597ef46c88f4f45b5a467b491f8c15fa5419f921 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Sat, 24 Aug 2024 11:47:49 +0200 Subject: [PATCH 13/18] Refactor: Clarify some variable names Now that we support repo subdirectories, what previously could be referred to as a "file name" should now (in most places) be referred to as a "repo-relative path", i.e. a path relative to the repository root. --- .../android/repos/DirectoryRepoTest.java | 4 +- .../orgzly/android/repos/DropboxRepoTest.java | 3 +- .../orgzly/android/repos/LocalDbRepoTest.java | 2 +- .../android/repos/RepoIgnoreNodeTest.kt | 4 +- .../com/orgzly/android/repos/SyncTest.java | 2 +- .../java/com/orgzly/android/BookName.java | 55 +++++++++------ .../java/com/orgzly/android/LocalStorage.java | 2 +- .../com/orgzly/android/data/DataRepository.kt | 22 +++--- .../android/git/GitFileSynchronizer.java | 70 +++++++++---------- .../orgzly/android/repos/DatabaseRepo.java | 10 +-- .../orgzly/android/repos/DirectoryRepo.java | 14 ++-- .../orgzly/android/repos/DocumentRepo.java | 38 +++++----- .../orgzly/android/repos/DropboxClient.java | 15 ++-- .../com/orgzly/android/repos/DropboxRepo.java | 18 ++--- .../com/orgzly/android/repos/GitRepo.java | 44 ++++++------ .../com/orgzly/android/repos/MockRepo.java | 27 +++++-- .../orgzly/android/repos/RepoIgnoreNode.kt | 2 +- .../com/orgzly/android/repos/RepoUtils.java | 4 +- .../com/orgzly/android/repos/SyncRepo.java | 18 +++-- .../com/orgzly/android/repos/WebdavRepo.kt | 24 +++---- .../com/orgzly/android/sync/BookNamesake.java | 4 +- .../java/com/orgzly/android/sync/SyncUtils.kt | 14 ++-- .../orgzly/android/ui/books/BooksFragment.kt | 4 +- .../orgzly/android/usecase/LinkFindTarget.kt | 2 +- .../com/orgzly/android/util/MiscUtils.java | 16 +++++ .../com/orgzly/android/util/UriUtils.java | 4 +- 26 files changed, 237 insertions(+), 185 deletions(-) diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java b/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java index f8d59232c..a26ae2a5c 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java @@ -63,7 +63,7 @@ public void testStoringFile() throws IOException { assertEquals(1, books.size()); assertEquals("booky", BookName.fromRook(books.get(0)).getName()); - assertEquals("booky.org", BookName.fromRook(books.get(0)).getFileName()); + assertEquals("booky.org", BookName.fromRook(books.get(0)).getRepoRelativePath()); assertEquals(repoUriString, books.get(0).getRepoUri().toString()); assertEquals(repoUriString + "/booky.org", books.get(0).getUri().toString()); } @@ -80,7 +80,7 @@ public void testExtension() throws IOException { assertEquals(1, books.size()); assertEquals("03", BookName.fromRook(books.get(0)).getName()); - assertEquals("03.org", BookName.fromRook(books.get(0)).getFileName()); + assertEquals("03.org", BookName.fromRook(books.get(0)).getRepoRelativePath()); assertEquals(13, books.get(0).getRepoId()); assertEquals(repoUriString, books.get(0).getRepoUri().toString()); assertEquals(repoUriString + "/03.org", books.get(0).getUri().toString()); diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java b/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java index 070e75c9a..6a85e56a5 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java @@ -137,8 +137,7 @@ public void testDropboxFileRename() throws IOException { assertEquals(1, repo.getBooks().size()); assertEquals(repo.getUri() + "/notebook-renamed.org", repo.getBooks().get(0).getUri().toString()); - assertEquals("notebook-renamed.org", - BookName.fromRook(repo.getBooks().get(0)).getFileName()); + assertEquals("notebook-renamed.org", BookName.fromRook(repo.getBooks().get(0)).getRepoRelativePath()); } private void uploadFileToRepo(Uri repoUri, String fileName, String fileContents) throws IOException { diff --git a/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java b/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java index dd54e3c02..c4d1ee6c8 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java @@ -58,7 +58,7 @@ public void testStoringBook() throws IOException { try { new NotesOrgExporter(dataRepository).exportBook(book, tmpFile); repo = testUtils.repoInstance(RepoType.MOCK, "mock://repo-a"); - repo.storeBook(tmpFile, BookName.fileName(book.getName(), BookFormat.ORG)); + repo.storeBook(tmpFile, BookName.repoRelativePath(book.getName(), BookFormat.ORG)); } finally { tmpFile.delete(); } diff --git a/app/src/androidTest/java/com/orgzly/android/repos/RepoIgnoreNodeTest.kt b/app/src/androidTest/java/com/orgzly/android/repos/RepoIgnoreNodeTest.kt index 7f983087f..6f71b4547 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/RepoIgnoreNodeTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/repos/RepoIgnoreNodeTest.kt @@ -12,8 +12,8 @@ import java.util.HashMap class RepoIgnoreNodeTest : OrgzlyTest() { class MockRepoWithMockIgnoreFile : MockRepo(repoWithProps, null) { - override fun openRepoFileInputStream(filePath: String): InputStream { - if (filePath == RepoIgnoreNode.IGNORE_FILE) { + override fun openRepoFileInputStream(repoRelativePath: String): InputStream { + if (repoRelativePath == RepoIgnoreNode.IGNORE_FILE) { val ignoreFileContents = """ IgnoredAnywhere.org /OnlyIgnoredInRoot.org diff --git a/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java b/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java index ff0be9b26..31fa1aaca 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java @@ -431,7 +431,7 @@ public void testDirectoryFileRename() throws IOException { assertEquals(1, repo.getBooks().size()); assertEquals(repo.getUri() + "/notebook-renamed.org", repo.getBooks().get(0).getUri().toString()); - assertEquals("notebook-renamed.org", BookName.fromRook(repo.getBooks().get(0)).getFileName()); + assertEquals("notebook-renamed.org", BookName.fromRook(repo.getBooks().get(0)).getRepoRelativePath()); LocalStorage.deleteRecursive(new File(repoDir)); } diff --git a/app/src/main/java/com/orgzly/android/BookName.java b/app/src/main/java/com/orgzly/android/BookName.java index e2cf421af..be99f2ed1 100644 --- a/app/src/main/java/com/orgzly/android/BookName.java +++ b/app/src/main/java/com/orgzly/android/BookName.java @@ -7,6 +7,7 @@ import com.orgzly.BuildConfig; import com.orgzly.android.db.entity.BookView; import com.orgzly.android.repos.Rook; +import com.orgzly.android.repos.VersionedRook; import com.orgzly.android.util.LogUtils; import java.util.regex.Matcher; @@ -22,34 +23,39 @@ public class BookName { private static final Pattern PATTERN = Pattern.compile("(.*)\\.(org)(\\.txt)?$"); private static final Pattern SKIP_PATTERN = Pattern.compile("^\\.#.*"); - private final String mFileName; + private final String mRepoRelativePath; private final String mName; private final BookFormat mFormat; - private BookName(String fileName, String name, BookFormat format) { - mFileName = fileName; + private BookName(String repoRelativePath, String name, BookFormat format) { + mRepoRelativePath = repoRelativePath; mName = name; mFormat = format; } - public static String getFileName(BookView bookView) { + public static String getRepoRelativePath(BookView bookView) { if (bookView.getSyncedTo() != null) { - return getFileName(bookView.getSyncedTo().getRepoUri(), bookView.getSyncedTo().getUri()); - + VersionedRook vrook = bookView.getSyncedTo(); + return getRepoRelativePath(vrook.getRepoUri(), vrook.getUri()); } else { - return fileName(bookView.getBook().getName(), BookFormat.ORG); + // There is no remote book; we can only guess the repo path from the book's name. + return repoRelativePath(bookView.getBook().getName(), BookFormat.ORG); } } + /** + * Used when creating a Book from an imported file. + * @param context Used for getting a DocumentFile, if possible + * @param uri URI provided by the file picker + * @return The book's file name + */ public static String getFileName(Context context, Uri uri) { String fileName; - DocumentFile documentFile = DocumentFile.fromSingleUri(context, uri); if ("content".equals(uri.getScheme()) && documentFile != null) { // Try using DocumentFile first (KitKat and above) fileName = documentFile.getName(); - } else { // Just get the last path segment fileName = uri.getLastPathSegment(); } @@ -63,7 +69,7 @@ public static String getFileName(Context context, Uri uri) { return fileName; } - public static String getFileName(Uri repoUri, Uri fileUri) { + public static String getRepoRelativePath(Uri repoUri, Uri fileUri) { /* The content:// repository type requires special handling */ if ("content".equals(repoUri.getScheme())) { String repoUriLastSegment = repoUri.toString().replaceAll("^.*/", ""); @@ -79,37 +85,44 @@ public static String getFileName(Uri repoUri, Uri fileUri) { } public static BookName fromRook(Rook rook) { - return fromFileName(getFileName(rook.getRepoUri(), rook.getUri())); + return fromRepoRelativePath(getRepoRelativePath(rook.getRepoUri(), rook.getUri())); } - public static boolean isSupportedFormatFileName(String fileName) { - return PATTERN.matcher(fileName).matches() && !SKIP_PATTERN.matcher(fileName).matches(); + public static boolean isSupportedFormatFileName(String path) { + return PATTERN.matcher(path).matches() && !SKIP_PATTERN.matcher(path).matches(); } - public static String fileName(String name, BookFormat format) { + public static String repoRelativePath(String name, BookFormat format) { if (format == BookFormat.ORG) { return name + ".org"; + } else { + throw new IllegalArgumentException("Unsupported format " + format); + } + } + public static String lastPathSegment(String name, BookFormat format) { + if (format == BookFormat.ORG) { + return Uri.parse(name).getLastPathSegment() + ".org"; } else { throw new IllegalArgumentException("Unsupported format " + format); } } - public static BookName fromFileName(String fileName) { - if (fileName != null) { - Matcher m = PATTERN.matcher(fileName); + public static BookName fromRepoRelativePath(String repoRelativePath) { + if (repoRelativePath != null) { + Matcher m = PATTERN.matcher(repoRelativePath); if (m.find()) { String name = m.group(1); String extension = m.group(2); if (extension.equals("org")) { - return new BookName(fileName, name, BookFormat.ORG); + return new BookName(repoRelativePath, name, BookFormat.ORG); } } } - throw new IllegalArgumentException("Unsupported book file name " + fileName); + throw new IllegalArgumentException("Unsupported book file name " + repoRelativePath); } public String getName() { @@ -120,8 +133,8 @@ public BookFormat getFormat() { return mFormat; } - public String getFileName() { - return mFileName; + public String getRepoRelativePath() { + return mRepoRelativePath; } } diff --git a/app/src/main/java/com/orgzly/android/LocalStorage.java b/app/src/main/java/com/orgzly/android/LocalStorage.java index 360237600..c3f476753 100644 --- a/app/src/main/java/com/orgzly/android/LocalStorage.java +++ b/app/src/main/java/com/orgzly/android/LocalStorage.java @@ -34,7 +34,7 @@ public LocalStorage(Context context) { * @throws IOException if external directory is not available */ public File getExportFile(String name, BookFormat format) throws IOException { - return new File(downloadsDirectory(), BookName.fileName(name, format)); + return new File(downloadsDirectory(), BookName.repoRelativePath(name, format)); } /** diff --git a/app/src/main/java/com/orgzly/android/data/DataRepository.kt b/app/src/main/java/com/orgzly/android/data/DataRepository.kt index 0861b2007..3b93329b0 100644 --- a/app/src/main/java/com/orgzly/android/data/DataRepository.kt +++ b/app/src/main/java/com/orgzly/android/data/DataRepository.kt @@ -80,9 +80,9 @@ class DataRepository @Inject constructor( BookAction.Type.PROGRESS, resources.getString(R.string.force_loading_from_uri, book.linkRepo.url))) - val fileName = BookName.getFileName(book) + val repoRelativePath = BookName.getRepoRelativePath(book) - val loadedBook = loadBookFromRepo(book.linkRepo.id, book.linkRepo.type, book.linkRepo.url, fileName) + val loadedBook = loadBookFromRepo(book.linkRepo.id, book.linkRepo.type, book.linkRepo.url, repoRelativePath) setBookLastActionAndSyncStatus(loadedBook!!.book.id, BookAction.forNow( BookAction.Type.INFO, @@ -103,7 +103,7 @@ class DataRepository @Inject constructor( val book = getBookView(bookId) ?: throw IOException(resources.getString(R.string.book_does_not_exist_anymore)) - val repositoryPath: String = BookName.getFileName(book) + val repositoryPath: String = BookName.getRepoRelativePath(book) try { /* Prefer link. */ @@ -386,7 +386,7 @@ class DataRepository @Inject constructor( /* Do not rename if the new filename will be ignored */ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - RepoUtils.ensureFileNameIsNotIgnored(repo, BookName.fileName(name, BookFormat.ORG)) + RepoUtils.ensurePathIsNotIgnored(repo, BookName.repoRelativePath(name, BookFormat.ORG)) val movedVrook = repo.renameBook(vrook.uri, name) @@ -513,8 +513,8 @@ class DataRepository @Inject constructor( // Ensure that the resulting file name is not ignored in this repo val syncRepo = getRepoInstance(repo.id, repo.type, repo.url) val bookName = getBook(bookId)!!.name - val fileName = BookName.fileName(bookName, BookFormat.ORG) - RepoUtils.ensureFileNameIsNotIgnored(syncRepo, fileName) + val repoRelativePath = BookName.repoRelativePath(bookName, BookFormat.ORG) + RepoUtils.ensurePathIsNotIgnored(syncRepo, repoRelativePath) } db.bookLink().upsert(bookId, repoId) @@ -1625,13 +1625,13 @@ class DataRepository @Inject constructor( @Throws(IOException::class) fun loadBookFromRepo(rook: Rook): BookView? { - val fileName = BookName.getFileName(rook.repoUri, rook.uri) + val repoRelativePath = BookName.getRepoRelativePath(rook.repoUri, rook.uri) - return loadBookFromRepo(rook.repoId, rook.repoType, rook.repoUri.toString(), fileName) + return loadBookFromRepo(rook.repoId, rook.repoType, rook.repoUri.toString(), repoRelativePath) } @Throws(IOException::class) - fun loadBookFromRepo(repoId: Long, repoType: RepoType, repoUrl: String, fileName: String): BookView? { + fun loadBookFromRepo(repoId: Long, repoType: RepoType, repoUrl: String, repoRelativePath: String): BookView? { val book: BookView? val repo = getRepoInstance(repoId, repoType, repoUrl) @@ -1639,9 +1639,9 @@ class DataRepository @Inject constructor( val tmpFile = getTempBookFile() try { /* Download from repo. */ - val vrook = repo.retrieveBook(fileName, tmpFile) + val vrook = repo.retrieveBook(repoRelativePath, tmpFile) - val bookName = BookName.fromFileName(fileName) + val bookName = BookName.fromRepoRelativePath(repoRelativePath) /* Store from file to Shelf. */ book = loadBookFromFile(bookName.name, bookName.format, tmpFile, vrook) diff --git a/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java b/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java index 21c340a60..f626246a6 100644 --- a/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java +++ b/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java @@ -60,11 +60,11 @@ private GitTransportSetter transportSetter() { public void retrieveLatestVersionOfFile( String repositoryPath, File destination) throws IOException { - MiscUtils.copyFile(repoDirectoryFile(repositoryPath), destination); + MiscUtils.copyFile(workTreeFile(repositoryPath), destination); } public InputStream openRepoFileInputStream(String repositoryPath) throws FileNotFoundException { - return new FileInputStream(repoDirectoryFile(repositoryPath)); + return new FileInputStream(workTreeFile(repositoryPath)); } private void fetch() throws IOException { @@ -119,13 +119,13 @@ private String createMergeBranchName(String repositoryPath, ObjectId commitHash) } public boolean updateAndCommitFileFromRevisionAndMerge( - File sourceFile, String repositoryPath, + File sourceFile, String repoRelativePath, ObjectId fileRevision, RevCommit revision) throws IOException { ensureRepoIsClean(); - if (updateAndCommitFileFromRevision(sourceFile, repositoryPath, fileRevision)) { + if (updateAndCommitFileFromRevision(sourceFile, repoRelativePath, fileRevision)) { if (BuildConfig.LOG_DEBUG) { - LogUtils.d(TAG, String.format("File '%s' committed without conflicts.", repositoryPath)); + LogUtils.d(TAG, String.format("File '%s' committed without conflicts.", repoRelativePath)); } return true; } @@ -134,7 +134,7 @@ public boolean updateAndCommitFileFromRevisionAndMerge( if (BuildConfig.LOG_DEBUG) { LogUtils.d(TAG, String.format("originalBranch is set to %s", originalBranch)); } - String mergeBranch = createMergeBranchName(repositoryPath, fileRevision); + String mergeBranch = createMergeBranchName(repoRelativePath, fileRevision); if (BuildConfig.LOG_DEBUG) { LogUtils.d(TAG, String.format("originalBranch is set to %s", originalBranch)); LogUtils.d(TAG, String.format("Temporary mergeBranch is set to %s", mergeBranch)); @@ -157,12 +157,12 @@ public boolean updateAndCommitFileFromRevisionAndMerge( setStartPoint(branchStartPoint).setName(mergeBranch).call(); if (!currentHead().equals(branchStartPoint)) throw new IOException("Failed to create new branch at " + branchStartPoint.toString()); - if (!updateAndCommitFileFromRevision(sourceFile, repositoryPath, fileRevision)) + if (!updateAndCommitFileFromRevision(sourceFile, repoRelativePath, fileRevision)) throw new IOException( String.format( "The provided file revision %s for %s is " + "not the same as the one found in the provided commit %s.", - fileRevision.toString(), repositoryPath, revision.toString())); + fileRevision.toString(), repoRelativePath, revision.toString())); mergeSucceeded = doMerge(mergeTarget); if (mergeSucceeded) { RevCommit merged = currentHead(); @@ -287,11 +287,11 @@ private void gitResetMerge() throws IOException, GitAPIException { } public boolean updateAndCommitFileFromRevision( - File sourceFile, String repositoryPath, ObjectId revision) throws IOException { + File sourceFile, String repoRelativePath, ObjectId revision) throws IOException { ensureRepoIsClean(); - ObjectId repositoryRevision = getFileRevision(repositoryPath, currentHead()); + ObjectId repositoryRevision = getFileRevision(repoRelativePath, currentHead()); if (repositoryRevision.equals(revision)) { - updateAndCommitFile(sourceFile, repositoryPath); + updateAndCommitFile(sourceFile, repoRelativePath); return true; } return false; @@ -377,7 +377,7 @@ public boolean attemptReturnToMainBranch() throws IOException { public void updateAndCommitExistingFile(File sourceFile, String repositoryPath) throws IOException { ensureRepoIsClean(); - File destinationFile = repoDirectoryFile(repositoryPath); + File destinationFile = workTreeFile(repositoryPath); if (!destinationFile.exists()) { throw new FileNotFoundException("File " + destinationFile + " does not exist"); } @@ -392,7 +392,7 @@ public void updateAndCommitExistingFile(File sourceFile, String repositoryPath) */ public void addAndCommitNewFile(File sourceFile, String repositoryPath) throws IOException { ensureRepoIsClean(); - File destinationFile = repoDirectoryFile(repositoryPath); + File destinationFile = workTreeFile(repositoryPath); if (destinationFile.exists()) { throw new IOException("Can't add new file " + repositoryPath + " that already exists."); } @@ -402,7 +402,7 @@ public void addAndCommitNewFile(File sourceFile, String repositoryPath) throws I private void ensureDirectoryHierarchy(String repositoryPath) throws IOException { if (repositoryPath.contains("/")) { - File targetDir = repoDirectoryFile(repositoryPath).getParentFile(); + File targetDir = workTreeFile(repositoryPath).getParentFile(); if (!(targetDir.exists() || targetDir.mkdirs())) { throw new IOException("The directory " + targetDir.getAbsolutePath() + " could " + "not be created"); @@ -411,13 +411,13 @@ private void ensureDirectoryHierarchy(String repositoryPath) throws IOException } private void updateAndCommitFile( - File sourceFile, String fileName) throws IOException { - File destinationFile = repoDirectoryFile(fileName); + File sourceFile, String repoRelativePath) throws IOException { + File destinationFile = workTreeFile(repoRelativePath); MiscUtils.copyFile(sourceFile, destinationFile); try { - git.add().addFilepattern(fileName).call(); + git.add().addFilepattern(repoRelativePath).call(); if (!gitRepoIsClean()) - commit(String.format("Orgzly update: %s", fileName)); + commit(String.format("Orgzly update: %s", repoRelativePath)); } catch (GitAPIException e) { throw new IOException("Failed to commit changes."); } @@ -443,11 +443,11 @@ public RevCommit getCommit(String identifier) throws IOException { } public RevCommit getLastCommitOfFile(Uri uri) throws GitAPIException { - String fileName = uri.toString().replaceFirst("^/", ""); - return git.log().setMaxCount(1).addPath(fileName).call().iterator().next(); + String repoRelativePath = uri.toString().replaceFirst("^/", ""); + return git.log().setMaxCount(1).addPath(repoRelativePath).call().iterator().next(); } - public String repoPath() { + public String workTreePath() { return git.getRepository().getWorkTree().getAbsolutePath(); } @@ -465,8 +465,8 @@ private void ensureRepoIsClean() throws IOException { throw new IOException("Refusing to update because there are uncommitted changes."); } - public File repoDirectoryFile(String filePath) { - return new File(repoPath(), filePath); + public File workTreeFile(String filePath) { + return new File(workTreePath(), filePath); } public boolean isEmptyRepo() throws IOException{ @@ -480,38 +480,38 @@ public ObjectId getFileRevision(String pathString, RevCommit commit) throws IOEx public boolean deleteFileFromRepo(Uri uri) throws IOException { if (mergeWithRemote()) { - String fileName = uri.toString().replaceFirst("^/", ""); + String repoRelativePath = uri.toString().replaceFirst("^/", ""); try { - git.rm().addFilepattern(fileName).call(); + git.rm().addFilepattern(repoRelativePath).call(); if (!gitRepoIsClean()) - commit(String.format("Orgzly deletion: %s", fileName)); + commit(String.format("Orgzly deletion: %s", repoRelativePath)); return true; } catch (GitAPIException e) { - throw new IOException(String.format("Failed to commit deletion of %s, %s", fileName, e.getMessage())); + throw new IOException(String.format("Failed to commit deletion of %s, %s", repoRelativePath, e.getMessage())); } } else { return false; } } - public boolean renameFileInRepo(String oldFileName, String newFileName) throws IOException { + public boolean renameFileInRepo(String oldPath, String newPath) throws IOException { ensureRepoIsClean(); if (mergeWithRemote()) { - File oldFile = repoDirectoryFile(oldFileName); - File newFile = repoDirectoryFile(newFileName); + File oldFile = workTreeFile(oldPath); + File newFile = workTreeFile(newPath); // Abort if destination file exists if (newFile.exists()) { - throw new IOException("Repository file " + newFileName + " already exists."); + throw new IOException("Repository file " + newPath + " already exists."); } - ensureDirectoryHierarchy(newFileName); + ensureDirectoryHierarchy(newPath); // Copy the file contents and add it to the index MiscUtils.copyFile(oldFile, newFile); try { - git.add().addFilepattern(newFileName).call(); + git.add().addFilepattern(newPath).call(); if (!gitRepoIsClean()) { // Remove the old file from the Git index - git.rm().addFilepattern(oldFileName).call(); - commit(String.format("Orgzly: rename %s to %s", oldFileName, newFileName)); + git.rm().addFilepattern(oldPath).call(); + commit(String.format("Orgzly: rename %s to %s", oldPath, newPath)); return true; } } catch (GitAPIException e) { diff --git a/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java b/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java index 10b81b958..a4f575f28 100644 --- a/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java @@ -48,24 +48,24 @@ public List getBooks() { } @Override - public VersionedRook retrieveBook(String fileName, File file) { - Uri uri = repoUri.buildUpon().appendPath(fileName).build(); + public VersionedRook retrieveBook(String repoRelativePath, File file) { + Uri uri = repoUri.buildUpon().appendPath(repoRelativePath).build(); return dbRepo.retrieveBook(repoId, repoUri, uri, file); } @Override - public InputStream openRepoFileInputStream(String fileName) throws IOException { + public InputStream openRepoFileInputStream(String repoRelativePath) throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override - public VersionedRook storeBook(File file, String fileName) throws IOException { + public VersionedRook storeBook(File file, String repoRelativePath) throws IOException { String content = MiscUtils.readStringFromFile(file); String rev = "MockedRevision-" + System.currentTimeMillis(); long mtime = System.currentTimeMillis(); - Uri uri = repoUri.buildUpon().appendPath(fileName).build(); + Uri uri = repoUri.buildUpon().appendPath(repoRelativePath).build(); VersionedRook vrook = new VersionedRook(repoId, RepoType.MOCK, repoUri, uri, rev, mtime); diff --git a/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java b/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java index c2be91cbc..aaaddca22 100644 --- a/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java @@ -124,8 +124,8 @@ public List getBooks() { } @Override - public VersionedRook retrieveBook(String fileName, File destinationFile) throws IOException { - Uri uri = repoUri.buildUpon().appendPath(fileName).build(); + public VersionedRook retrieveBook(String repoRelativePath, File destinationFile) throws IOException { + Uri uri = repoUri.buildUpon().appendPath(repoRelativePath).build(); String path = uri.getPath(); @@ -145,17 +145,17 @@ public VersionedRook retrieveBook(String fileName, File destinationFile) throws } @Override - public InputStream openRepoFileInputStream(String fileName) throws IOException { - return new FileInputStream(repoUri.buildUpon().appendPath(fileName).build().getPath()); + public InputStream openRepoFileInputStream(String repoRelativePath) throws IOException { + return new FileInputStream(repoUri.buildUpon().appendPath(repoRelativePath).build().getPath()); } @Override - public VersionedRook storeBook(File file, String fileName) throws IOException { + public VersionedRook storeBook(File file, String repoRelativePath) throws IOException { if (!file.exists()) { throw new FileNotFoundException("File " + file + " does not exist"); } - File destinationFile = new File(mDirectory, fileName); + File destinationFile = new File(mDirectory, repoRelativePath); File destinationFileParent = destinationFile.getParentFile(); @@ -172,7 +172,7 @@ public VersionedRook storeBook(File file, String fileName) throws IOException { String rev = String.valueOf(destinationFile.lastModified()); long mtime = destinationFile.lastModified(); - Uri uri = repoUri.buildUpon().appendPath(fileName).build(); + Uri uri = repoUri.buildUpon().appendPath(repoRelativePath).build(); return new VersionedRook(repoId, RepoType.DIRECTORY, repoUri, uri, rev, mtime); } diff --git a/app/src/main/java/com/orgzly/android/repos/DocumentRepo.java b/app/src/main/java/com/orgzly/android/repos/DocumentRepo.java index 1062d1bd7..03dc61eca 100644 --- a/app/src/main/java/com/orgzly/android/repos/DocumentRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DocumentRepo.java @@ -115,17 +115,17 @@ private List walkFileTree() { while (!directoryNodes.isEmpty()) { DocumentFile currentDir = directoryNodes.remove(0); for (DocumentFile node : currentDir.listFiles()) { - String relativeFileName = BookName.getFileName(repoUri, node.getUri()); + String repoRelativePath = BookName.getRepoRelativePath(repoUri, node.getUri()); if (node.isDirectory()) { if (Build.VERSION.SDK_INT >= 26) { - if (ignores.isPathIgnored(relativeFileName, true)) { + if (ignores.isPathIgnored(repoRelativePath, true)) { continue; } } directoryNodes.add(node); } else { if (Build.VERSION.SDK_INT >= 26) { - if (ignores.isPathIgnored(relativeFileName, false)) { + if (ignores.isPathIgnored(repoRelativePath, false)) { continue; } } result.add(node); @@ -135,19 +135,19 @@ private List walkFileTree() { return result; } - private DocumentFile getDocumentFileFromFileName(String fileName) { - String fullUri = repoDocumentFile.getUri() + Uri.encode("/" + fileName); + private DocumentFile getDocumentFileFromPath(String path) { + String fullUri = repoDocumentFile.getUri() + Uri.encode("/" + path); return DocumentFile.fromSingleUri(context, Uri.parse(fullUri)); } @Override - public VersionedRook retrieveBook(String fileName, File destinationFile) throws IOException { - DocumentFile sourceFile = getDocumentFileFromFileName(fileName); + public VersionedRook retrieveBook(String repoRelativePath, File destinationFile) throws IOException { + DocumentFile sourceFile = getDocumentFileFromPath(repoRelativePath); if (sourceFile == null) { - throw new FileNotFoundException("Book " + fileName + " not found in " + repoUri); + throw new FileNotFoundException("Book " + repoRelativePath + " not found in " + repoUri); } else { if (BuildConfig.LOG_DEBUG) { - LogUtils.d(TAG, "Found DocumentFile for " + fileName + ": " + sourceFile.getUri()); + LogUtils.d(TAG, "Found DocumentFile for " + repoRelativePath + ": " + sourceFile.getUri()); } } @@ -163,27 +163,27 @@ public VersionedRook retrieveBook(String fileName, File destinationFile) throws } @Override - public InputStream openRepoFileInputStream(String fileName) throws IOException { - DocumentFile sourceFile = getDocumentFileFromFileName(fileName); + public InputStream openRepoFileInputStream(String repoRelativePath) throws IOException { + DocumentFile sourceFile = getDocumentFileFromPath(repoRelativePath); if (!sourceFile.exists()) throw new FileNotFoundException(); return context.getContentResolver().openInputStream(sourceFile.getUri()); } @Override - public VersionedRook storeBook(File file, String path) throws IOException { + public VersionedRook storeBook(File file, String repoRelativePath) throws IOException { if (!file.exists()) { throw new FileNotFoundException("File " + file + " does not exist"); } - DocumentFile destinationFile = getDocumentFileFromFileName(path); - if (path.contains("/")) { - DocumentFile destinationDir = ensureDirectoryHierarchy(path); - String fileName = Uri.parse(path).getLastPathSegment(); + DocumentFile destinationFile = getDocumentFileFromPath(repoRelativePath); + if (repoRelativePath.contains("/")) { + DocumentFile destinationDir = ensureDirectoryHierarchy(repoRelativePath); + String fileName = Uri.parse(repoRelativePath).getLastPathSegment(); if (destinationDir.findFile(fileName) == null) { destinationFile = destinationDir.createFile("text/*", fileName); } } else { if (!destinationFile.exists()) { - repoDocumentFile.createFile("text/*", path); + repoDocumentFile.createFile("text/*", repoRelativePath); } } @@ -239,8 +239,8 @@ public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOExcepti "" ) ); - BookName oldBookName = BookName.fromFileName(BookName.getFileName(repoUri, oldFullUri)); - String newRelativePath = BookName.fileName(newName, oldBookName.getFormat()); + BookName oldBookName = BookName.fromRepoRelativePath(BookName.getRepoRelativePath(repoUri, oldFullUri)); + String newRelativePath = BookName.repoRelativePath(newName, oldBookName.getFormat()); String newDocFileName = Uri.parse(newRelativePath).getLastPathSegment(); DocumentFile newDir; Uri newUri = oldFullUri; diff --git a/app/src/main/java/com/orgzly/android/repos/DropboxClient.java b/app/src/main/java/com/orgzly/android/repos/DropboxClient.java index 745384b02..fceec62ee 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxClient.java +++ b/app/src/main/java/com/orgzly/android/repos/DropboxClient.java @@ -12,6 +12,7 @@ import com.dropbox.core.json.JsonReadException; import com.dropbox.core.oauth.DbxCredential; import com.dropbox.core.v2.DbxClientV2; +import com.dropbox.core.v2.files.DeleteResult; import com.dropbox.core.v2.files.FileMetadata; import com.dropbox.core.v2.files.FolderMetadata; import com.dropbox.core.v2.files.GetMetadataErrorException; @@ -219,18 +220,18 @@ public List getBooks(Uri repoUri, RepoIgnoreNode ignores) throws return list; } - private Uri getFullUriFromRelativePath(Uri repoUri, String relativePath) { - String encodedFileName = Uri.encode(relativePath, "/"); - return Uri.withAppendedPath(repoUri, encodedFileName); + private Uri getFullUriFromRelativePath(Uri repoUri, String repoRelativePath) { + String encodedPath = Uri.encode(repoRelativePath, "/"); + return Uri.withAppendedPath(repoUri, encodedPath); } /** * Download file from Dropbox and store it to a local file. */ - public VersionedRook download(Uri repoUri, String relativePath, File localFile) throws IOException { + public VersionedRook download(Uri repoUri, String repoRelativePath, File localFile) throws IOException { linkedOrThrow(); - Uri uri = getFullUriFromRelativePath(repoUri, relativePath); + Uri uri = getFullUriFromRelativePath(repoUri, repoRelativePath); OutputStream out = new BufferedOutputStream(new FileOutputStream(localFile)); @@ -262,10 +263,10 @@ public VersionedRook download(Uri repoUri, String relativePath, File localFile) } } - public InputStream streamFile(Uri repoUri, String fileName) throws IOException { + public InputStream streamFile(Uri repoUri, String repoRelativePath) throws IOException { linkedOrThrow(); - Uri uri = repoUri.buildUpon().appendPath(fileName).build(); + Uri uri = repoUri.buildUpon().appendPath(repoRelativePath).build(); FileMetadata metadata; String rev; DbxDownloader downloader; diff --git a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java index 1dbe611ab..21cfdf65c 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java @@ -45,24 +45,24 @@ public List getBooks() throws IOException { } @Override - public VersionedRook retrieveBook(String fileName, File file) throws IOException { - return client.download(repoUri, fileName, file); + public VersionedRook retrieveBook(String repoRelativePath, File file) throws IOException { + return client.download(repoUri, repoRelativePath, file); } @Override - public InputStream openRepoFileInputStream(String fileName) throws IOException { - return client.streamFile(repoUri, fileName); + public InputStream openRepoFileInputStream(String repoRelativePath) throws IOException { + return client.streamFile(repoUri, repoRelativePath); } @Override - public VersionedRook storeBook(File file, String fileName) throws IOException { - return client.upload(file, repoUri, fileName); + public VersionedRook storeBook(File file, String repoRelativePath) throws IOException { + return client.upload(file, repoUri, repoRelativePath); } @Override public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException { - BookName oldBookName = BookName.fromFileName(BookName.getFileName(repoUri, oldFullUri)); - String newRelativePath = BookName.fileName(newName, oldBookName.getFormat()); + BookName oldBookName = BookName.fromRepoRelativePath(BookName.getRepoRelativePath(repoUri, oldFullUri)); + String newRelativePath = BookName.repoRelativePath(newName, oldBookName.getFormat()); String newEncodedRelativePath = Uri.encode(newRelativePath, "/"); Uri newFullUri = repoUri.buildUpon().appendEncodedPath(newEncodedRelativePath).build(); return client.move(repoUri, oldFullUri, newFullUri); @@ -74,7 +74,7 @@ public void delete(Uri uri) throws IOException { } /** - * Intended for tests. The delete() method does not allow deleting directories. + * Only used by tests. The delete() method does not allow deleting directories. */ public void deleteDirectory(Uri uri) throws IOException { client.deleteFolder(uri.getPath()); diff --git a/app/src/main/java/com/orgzly/android/repos/GitRepo.java b/app/src/main/java/com/orgzly/android/repos/GitRepo.java index 1e0b6bddf..2cf1941e1 100644 --- a/app/src/main/java/com/orgzly/android/repos/GitRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/GitRepo.java @@ -185,16 +185,16 @@ public boolean isAutoSyncSupported() { return true; } - public VersionedRook storeBook(File file, String repositoryPath) throws IOException { - File destination = synchronizer.repoDirectoryFile(repositoryPath); + public VersionedRook storeBook(File file, String repoRelativePath) throws IOException { + File destination = synchronizer.workTreeFile(repoRelativePath); if (destination.exists()) { - synchronizer.updateAndCommitExistingFile(file, repositoryPath); + synchronizer.updateAndCommitExistingFile(file, repoRelativePath); } else { - synchronizer.addAndCommitNewFile(file, repositoryPath); + synchronizer.addAndCommitNewFile(file, repoRelativePath); } synchronizer.tryPush(); - return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(repositoryPath).build()); + return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(repoRelativePath).build()); } private RevWalk walk() { @@ -206,9 +206,9 @@ RevCommit getCommitFromRevisionString(String revisionString) throws IOException } @Override - public VersionedRook retrieveBook(String fileName, File destination) throws IOException { + public VersionedRook retrieveBook(String repoRelativePath, File destination) throws IOException { - Uri sourceUri = Uri.parse("/" + fileName); + Uri sourceUri = Uri.parse("/" + repoRelativePath); // Ensure our repo copy is up-to-date. This is necessary when force-loading a book. synchronizer.mergeWithRemote(); @@ -219,8 +219,8 @@ public VersionedRook retrieveBook(String fileName, File destination) throws IOEx } @Override - public InputStream openRepoFileInputStream(String fileName) throws IOException { - Uri sourceUri = Uri.parse(fileName); + public InputStream openRepoFileInputStream(String repoRelativePath) throws IOException { + Uri sourceUri = Uri.parse(repoRelativePath); return synchronizer.openRepoFileInputStream(sourceUri.getPath()); } @@ -265,12 +265,12 @@ public List getBooks() throws IOException { public boolean include(TreeWalk walker) { final FileMode mode = walk.getFileMode(); final boolean isDirectory = mode == FileMode.TREE; - final String filePath = walk.getPathString(); - if (ignores.isIgnored(filePath, isDirectory) == IgnoreNode.MatchResult.IGNORED) + final String repoRelativePath = walk.getPathString(); + if (ignores.isIgnored(repoRelativePath, isDirectory) == IgnoreNode.MatchResult.IGNORED) return false; if (isDirectory) return true; - return BookName.isSupportedFormatFileName(filePath); + return BookName.isSupportedFormatFileName(repoRelativePath); } @Override @@ -298,11 +298,11 @@ public void delete(Uri uri) throws IOException { } public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException { - String oldFileName = oldFullUri.toString().replaceFirst("^/", ""); - String newFileName = BookName.fileName(newName, BookFormat.ORG); - if (synchronizer.renameFileInRepo(oldFileName, newFileName)) { + String oldPath = oldFullUri.toString().replaceFirst("^/", ""); + String newPath = BookName.repoRelativePath(newName, BookFormat.ORG); + if (synchronizer.renameFileInRepo(oldPath, newPath)) { synchronizer.tryPush(); - return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(newFileName).build()); + return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(newPath).build()); } else { return null; } @@ -311,16 +311,16 @@ public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOExcepti @Override public TwoWaySyncResult syncBook( Uri uri, VersionedRook current, File fromDB) throws IOException { - String fileName = uri.getPath().replaceFirst("^/", ""); + String repoRelativePath = uri.getPath().replaceFirst("^/", ""); boolean merged = true; if (current != null) { RevCommit rookCommit = getCommitFromRevisionString(current.getRevision()); if (BuildConfig.LOG_DEBUG) { - LogUtils.d(TAG, String.format("Syncing file %s, rookCommit: %s", fileName, rookCommit)); + LogUtils.d(TAG, String.format("Syncing file %s, rookCommit: %s", repoRelativePath, rookCommit)); } merged = synchronizer.updateAndCommitFileFromRevisionAndMerge( - fromDB, fileName, - synchronizer.getFileRevision(fileName, rookCommit), + fromDB, repoRelativePath, + synchronizer.getFileRevision(repoRelativePath, rookCommit), rookCommit); if (merged) { @@ -333,9 +333,9 @@ public TwoWaySyncResult syncBook( } else { Log.w(TAG, "Unable to find previous commit, loading from repository."); } - File writeBackFile = synchronizer.repoDirectoryFile(fileName); + File writeBackFile = synchronizer.workTreeFile(repoRelativePath); return new TwoWaySyncResult( - currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(fileName).build()), merged, + currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(repoRelativePath).build()), merged, writeBackFile); } diff --git a/app/src/main/java/com/orgzly/android/repos/MockRepo.java b/app/src/main/java/com/orgzly/android/repos/MockRepo.java index 35f8b9cea..df2bedbb0 100644 --- a/app/src/main/java/com/orgzly/android/repos/MockRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/MockRepo.java @@ -4,9 +4,14 @@ import android.os.SystemClock; +import androidx.test.core.app.ApplicationProvider; + import com.orgzly.android.data.DbRepoBookRepository; +import com.orgzly.android.prefs.AppPreferences; +import java.io.ByteArrayInputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -25,10 +30,16 @@ public class MockRepo implements SyncRepo { private static final long SLEEP_FOR_STORE_BOOK = 200; private static final long SLEEP_FOR_DELETE_BOOK = 100; + public static final String IGNORE_RULES_PREF_KEY = "ignore_rules"; + + private String ignoreRules; + private DatabaseRepo databaseRepo; public MockRepo(RepoWithProps repoWithProps, DbRepoBookRepository dbRepo) { databaseRepo = new DatabaseRepo(repoWithProps, dbRepo); + ignoreRules = AppPreferences.repoPropsMap(ApplicationProvider.getApplicationContext(), + repoWithProps.getRepo().getId()).get(IGNORE_RULES_PREF_KEY); } @Override @@ -53,20 +64,24 @@ public List getBooks() throws IOException { } @Override - public VersionedRook retrieveBook(String fileName, File file) throws IOException { + public VersionedRook retrieveBook(String repoRelativePath, File file) throws IOException { SystemClock.sleep(SLEEP_FOR_RETRIEVE_BOOK); - return databaseRepo.retrieveBook(fileName, file); + return databaseRepo.retrieveBook(repoRelativePath, file); } @Override - public InputStream openRepoFileInputStream(String fileName) throws IOException { - throw new FileNotFoundException(); + public InputStream openRepoFileInputStream(String repoRelativePath) throws IOException { + if (repoRelativePath.equals(RepoIgnoreNode.IGNORE_FILE) && ignoreRules != null) { + return new ByteArrayInputStream(ignoreRules.getBytes()); + } else { + throw new FileNotFoundException(); + } } @Override - public VersionedRook storeBook(File file, String fileName) throws IOException { + public VersionedRook storeBook(File file, String repoRelativePath) throws IOException { SystemClock.sleep(SLEEP_FOR_STORE_BOOK); - return databaseRepo.storeBook(file, fileName); + return databaseRepo.storeBook(file, repoRelativePath); } @Override diff --git a/app/src/main/java/com/orgzly/android/repos/RepoIgnoreNode.kt b/app/src/main/java/com/orgzly/android/repos/RepoIgnoreNode.kt index d8fe16411..25e7826b8 100644 --- a/app/src/main/java/com/orgzly/android/repos/RepoIgnoreNode.kt +++ b/app/src/main/java/com/orgzly/android/repos/RepoIgnoreNode.kt @@ -44,7 +44,7 @@ class RepoIgnoreNode(repo: SyncRepo) : IgnoreNode() { } @RequiresApi(Build.VERSION_CODES.O) - fun ensureFileNameIsNotIgnored(filePath: String) { + fun ensurePathIsNotIgnored(filePath: String) { if (isPathIgnored(filePath, false)) { throw IOException( App.getAppContext().getString( diff --git a/app/src/main/java/com/orgzly/android/repos/RepoUtils.java b/app/src/main/java/com/orgzly/android/repos/RepoUtils.java index 639e77cc2..9c4f2942d 100644 --- a/app/src/main/java/com/orgzly/android/repos/RepoUtils.java +++ b/app/src/main/java/com/orgzly/android/repos/RepoUtils.java @@ -32,8 +32,8 @@ public static boolean isAutoSyncSupported(Collection repos) { } @RequiresApi(api = Build.VERSION_CODES.O) - public static void ensureFileNameIsNotIgnored(SyncRepo repo, String fileName) { - new RepoIgnoreNode(repo).ensureFileNameIsNotIgnored(fileName); + public static void ensurePathIsNotIgnored(SyncRepo repo, String repoRelativePath) { + new RepoIgnoreNode(repo).ensurePathIsNotIgnored(repoRelativePath); } } diff --git a/app/src/main/java/com/orgzly/android/repos/SyncRepo.java b/app/src/main/java/com/orgzly/android/repos/SyncRepo.java index 6009051a6..11d6cfb33 100644 --- a/app/src/main/java/com/orgzly/android/repos/SyncRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/SyncRepo.java @@ -31,23 +31,31 @@ public interface SyncRepo { /** * Download the latest available revision of the book and store its content to {@code File}. */ - VersionedRook retrieveBook(String fileName, File destination) throws IOException; + VersionedRook retrieveBook(String repoRelativePath, File destination) throws IOException; /** * Open a file in the repository for reading. Originally added for parsing the .orgzlyignore * file. - * @param fileName The file to open + * @param repoRelativePath The file to open * @throws IOException */ - InputStream openRepoFileInputStream(String fileName) throws IOException; + InputStream openRepoFileInputStream(String repoRelativePath) throws IOException; /** * Uploads book storing it under given filename under repo's url. * @param file The contents of this file should be stored at the remote location/repo - * @param fileName The contents ({@code file}) should be stored under this name + * @param repoRelativePath The contents ({@code file}) should be stored under this + * (non-encoded) name */ - VersionedRook storeBook(File file, String fileName) throws IOException; + VersionedRook storeBook(File file, String repoRelativePath) throws IOException; + /** + * + * @param oldFullUri Uri of the original repository file + * @param newName The new desired book name + * @return + * @throws IOException + */ VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException; // VersionedRook moveBook(Uri from, Uri uri) throws IOException; diff --git a/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt b/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt index a1be5f5ba..81599b893 100644 --- a/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt +++ b/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt @@ -186,8 +186,8 @@ class WebdavRepo( .toMutableList() } - override fun retrieveBook(fileName: String?, destination: File?): VersionedRook { - val fileUrl = Uri.withAppendedPath(uri, fileName).toUrl() + override fun retrieveBook(repoRelativePath: String?, destination: File?): VersionedRook { + val fileUrl = Uri.withAppendedPath(uri, repoRelativePath).toUrl() sardine.get(fileUrl).use { inputStream -> FileOutputStream(destination).use { outputStream -> @@ -198,8 +198,8 @@ class WebdavRepo( return sardine.list(fileUrl).first().toVersionedRook() } - override fun openRepoFileInputStream(fileName: String): InputStream { - val fileUrl = Uri.withAppendedPath(uri, fileName).toUrl() + override fun openRepoFileInputStream(repoRelativePath: String): InputStream { + val fileUrl = Uri.withAppendedPath(uri, repoRelativePath).toUrl() if (!sardine.exists(fileUrl)) throw FileNotFoundException() return sardine.get(fileUrl) @@ -218,14 +218,14 @@ class WebdavRepo( } } - override fun storeBook(file: File, fileName: String): VersionedRook { - val encodedFileName = Uri.encode(fileName, "/") - if (encodedFileName != null) { - if (encodedFileName.contains("/")) { - ensureDirectoryHierarchy(encodedFileName) + override fun storeBook(file: File, repoRelativePath: String): VersionedRook { + val encodedRepoPath = Uri.encode(repoRelativePath, "/") + if (encodedRepoPath != null) { + if (encodedRepoPath.contains("/")) { + ensureDirectoryHierarchy(encodedRepoPath) } } - val fileUrl = uri.buildUpon().appendEncodedPath(encodedFileName).build().toUrl() + val fileUrl = uri.buildUpon().appendEncodedPath(encodedRepoPath).build().toUrl() sardine.put(fileUrl, file, null) @@ -233,8 +233,8 @@ class WebdavRepo( } override fun renameBook(oldFullUri: Uri, newName: String): VersionedRook { - val oldBookName = BookName.fromFileName(BookName.getFileName(uri, oldFullUri)) - val newRelativePath = BookName.fileName(newName, oldBookName.format) + val oldBookName = BookName.fromRepoRelativePath(BookName.getRepoRelativePath(uri, oldFullUri)) + val newRelativePath = BookName.repoRelativePath(newName, oldBookName.format) val newEncodedRelativePath = Uri.encode(newRelativePath, "/") val newFullUrl = uri.buildUpon().appendEncodedPath(newEncodedRelativePath).build().toUrl() diff --git a/app/src/main/java/com/orgzly/android/sync/BookNamesake.java b/app/src/main/java/com/orgzly/android/sync/BookNamesake.java index eaa4390cc..7d86055f6 100644 --- a/app/src/main/java/com/orgzly/android/sync/BookNamesake.java +++ b/app/src/main/java/com/orgzly/android/sync/BookNamesake.java @@ -47,8 +47,8 @@ public static Map getAll(List books, List { repoEntity = dataRepository.getRepos().iterator().next() repoUrl = repoEntity.url - repositoryPath = BookName.fileName(namesake.book.book.name, BookFormat.ORG) + repositoryPath = BookName.repoRelativePath(namesake.book.book.name, BookFormat.ORG) /* Set repo link before saving to ensure repo ignore rules are checked */ dataRepository.setLink(namesake.book.book.id, repoEntity) dataRepository.saveBookToRepo(repoEntity, repositoryPath, namesake.book, BookFormat.ORG) @@ -165,7 +165,7 @@ object SyncUtils { BookSyncStatus.BOOK_WITH_LINK_LOCAL_MODIFIED -> { repoEntity = namesake.book.linkRepo repoUrl = repoEntity!!.url - repositoryPath = BookName.getFileName(repoUrl.toUri(), namesake.book.syncedTo!!.uri) + repositoryPath = BookName.getRepoRelativePath(repoUrl.toUri(), namesake.book.syncedTo!!.uri) dataRepository.saveBookToRepo(repoEntity, repositoryPath, namesake.book, BookFormat.ORG) bookAction = BookAction.forNow(BookAction.Type.INFO, namesake.status.msg(repoUrl)) } @@ -173,7 +173,7 @@ object SyncUtils { BookSyncStatus.ONLY_BOOK_WITH_LINK -> { repoEntity = namesake.book.linkRepo repoUrl = repoEntity!!.url - repositoryPath = BookName.fileName(namesake.book.book.name, BookFormat.ORG) + repositoryPath = BookName.repoRelativePath(namesake.book.book.name, BookFormat.ORG) dataRepository.saveBookToRepo(repoEntity, repositoryPath, namesake.book, BookFormat.ORG) bookAction = BookAction.forNow(BookAction.Type.INFO, namesake.status.msg(repoUrl)) } @@ -190,8 +190,8 @@ object SyncUtils { var noNewMergeConflicts = true // If there are only local changes, the GitRepo.syncBook method is overly complicated. if (namesake.status == BookSyncStatus.BOOK_WITH_LINK_LOCAL_MODIFIED) { - val fileName = BookName.getFileName(repo.getUri(), namesake.book.syncedTo!!.uri) - dataRepository.saveBookToRepo(namesake.book.linkRepo!!, fileName, namesake.book, BookFormat.ORG) + val repoRelativePath = BookName.getRepoRelativePath(repo.getUri(), namesake.book.syncedTo!!.uri) + dataRepository.saveBookToRepo(namesake.book.linkRepo!!, repoRelativePath, namesake.book, BookFormat.ORG) } else { val dbFile = dataRepository.getTempBookFile() try { @@ -202,8 +202,8 @@ object SyncUtils { newRook = newRook1 // We only need to write it if syncback is needed if (loadFile != null) { - val fileName = BookName.getFileName(repo.getUri(), newRook.uri) - val bookName = BookName.fromFileName(fileName) + val repoRelativePath = BookName.getRepoRelativePath(repo.getUri(), newRook.uri) + val bookName = BookName.fromRepoRelativePath(repoRelativePath) if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Loading from file '$loadFile'") dataRepository.loadBookFromFile( bookName.name, diff --git a/app/src/main/java/com/orgzly/android/ui/books/BooksFragment.kt b/app/src/main/java/com/orgzly/android/ui/books/BooksFragment.kt index b025568dd..68c4f91eb 100644 --- a/app/src/main/java/com/orgzly/android/ui/books/BooksFragment.kt +++ b/app/src/main/java/com/orgzly/android/ui/books/BooksFragment.kt @@ -312,7 +312,7 @@ class BooksFragment : CommonFragment(), DrawerItem, OnViewHolderClickListener Date: Sat, 24 Aug 2024 13:34:19 +0200 Subject: [PATCH 14/18] Add new tests and convert some repository tests to local JVM tests Create a generic test suite for SyncRepo implementations in the "shared-test" library. This allows writing "abstract" test cases which all implementations must pass. The main challenge in creating these "abstract" tests for SyncRepo was that most implementations can be tested with local unit tests, whereas DocumentRepo requires instrumented tests, since the Android class DocumentFile is so central to it. I tried several different approaches, but ended up with this separate "shared-test" library, containing an abstract class SyncRepoTest, which helps you by defining all the test cases and contains all their inner logic. I have also tried keeping all the tests as instrumented tests, but I wanted to launch a lightweight WebDAV server for those tests, and it was not possible to get that to work in Android. The WebDAV server was also the reason why I switched from Java 11 to Java 17 in the build process. But that was due, anyway, as Java 11 is now old and unsupported. --- .github/workflows/test.yaml | 46 +- app/build.gradle | 20 +- .../java/com/orgzly/android/OrgzlyTest.java | 2 - .../java/com/orgzly/android/TestUtils.java | 9 + .../orgzly/android/espresso/SyncingTest.java | 159 ------- .../android/repos/DataRepositoryTest.java | 1 + .../android/repos/DirectoryRepoTest.java | 1 - .../orgzly/android/repos/DocumentRepoTest.kt | 203 +++++++++ .../orgzly/android/repos/DropboxRepoTest.java | 103 +---- .../com/orgzly/android/repos/GitRepoTest.kt | 152 ------- .../com/orgzly/android/repos/SyncTest.java | 266 ++++++++++- .../orgzly/android/repos/DropboxRepoTest.kt | 135 ++++++ .../com/orgzly/android/repos/GitRepoTest.kt | 137 ++++++ .../orgzly/android/repos/WebdavRepoTest.kt | 138 ++++++ settings.gradle | 1 + shared-test/.gitignore | 1 + shared-test/build.gradle | 51 +++ shared-test/proguard-rules.pro | 21 + shared-test/src/main/AndroidManifest.xml | 17 + .../com/orgzly/android/repos/SyncRepoTest.kt | 422 ++++++++++++++++++ 20 files changed, 1449 insertions(+), 436 deletions(-) create mode 100644 app/src/androidTest/java/com/orgzly/android/repos/DocumentRepoTest.kt delete mode 100644 app/src/androidTest/java/com/orgzly/android/repos/GitRepoTest.kt create mode 100644 app/src/test/java/com/orgzly/android/repos/DropboxRepoTest.kt create mode 100644 app/src/test/java/com/orgzly/android/repos/GitRepoTest.kt create mode 100644 app/src/test/java/com/orgzly/android/repos/WebdavRepoTest.kt create mode 100644 shared-test/.gitignore create mode 100644 shared-test/build.gradle create mode 100644 shared-test/proguard-rules.pro create mode 100644 shared-test/src/main/AndroidManifest.xml create mode 100644 shared-test/src/main/java/com/orgzly/android/repos/SyncRepoTest.kt diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bb51aa23d..f58715c18 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,7 +22,41 @@ on: workflow_dispatch: jobs: - test: + + localUnitTests: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Gradle build Fdroid + run: ./gradlew assembleFdroidDebug + + - name: Gradle test Fdroid + run: ./gradlew testFdroidDebugUnitTest + + - name: Add Dropbox API credentials (for DropboxRepo tests) + shell: bash + run: | + echo "dropbox.refresh_token = \"${{ secrets.DROPBOX_REFRESH_TOKEN }}\"" >> app.properties + echo "dropbox.app_key = \"${{ secrets.DROPBOX_APP_KEY }}\"" >> app.properties + + - name: Gradle test Premium + run: ./gradlew testPremiumDebugUnitTest + + instrumentedTests: runs-on: ubuntu-latest strategy: matrix: @@ -39,6 +73,12 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 @@ -80,7 +120,7 @@ jobs: echo "dropbox.refresh_token = \"${{ secrets.DROPBOX_REFRESH_TOKEN }}\"" >> app.properties echo "dropbox.app_key = \"${{ secrets.DROPBOX_APP_KEY }}\"" >> app.properties - - name: Run tests + - name: Run instrumented tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} @@ -91,4 +131,4 @@ jobs: disable-spellchecker: true profile: Nexus 6 # Tests should use the build which includes Dropbox code. - script: ./gradlew connected${{matrix.flavor}}DebugAndroidTest --no-watch-fs --build-cache --info + script: ./gradlew --no-configuration-cache connected${{matrix.flavor}}DebugAndroidTest --no-watch-fs --build-cache --info diff --git a/app/build.gradle b/app/build.gradle index 27beb38f4..394d560ae 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,9 +38,11 @@ android { viewBinding true } -// testOptions { -// execution 'ANDROIDX_TEST_ORCHESTRATOR' -// } + testOptions { + unitTests { + includeAndroidResources = true + } + } buildTypes { release { @@ -91,12 +93,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = 11 + jvmTarget = 17 } packagingOptions { @@ -149,7 +151,9 @@ dependencies { implementation "androidx.work:work-runtime-ktx:$versions.android_workmanager" - testImplementation "junit:junit:$versions.junit" + testImplementation "androidx.test.ext:junit:$versions.android_test_ext_junit" + testImplementation 'org.robolectric:robolectric:4.13' + testImplementation "io.github.atetzner:webdav-embedded-server:0.2.1" // AndroidX Test androidTestImplementation "androidx.test.espresso:espresso-core:$versions.android_test_espresso" @@ -209,6 +213,8 @@ dependencies { implementation("androidx.biometric:biometric-ktx:$versions.biometric_ktx") { because 'Protect SSH key with biometric prompt' } + testImplementation(project(":shared-test")) + androidTestImplementation(project(":shared-test")) } repositories { diff --git a/app/src/androidTest/java/com/orgzly/android/OrgzlyTest.java b/app/src/androidTest/java/com/orgzly/android/OrgzlyTest.java index dea5ce2e8..42c7256ae 100644 --- a/app/src/androidTest/java/com/orgzly/android/OrgzlyTest.java +++ b/app/src/androidTest/java/com/orgzly/android/OrgzlyTest.java @@ -4,7 +4,6 @@ import android.Manifest; import android.app.Activity; -import android.app.UiAutomation; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; @@ -20,7 +19,6 @@ import com.orgzly.android.repos.RepoFactory; import com.orgzly.android.util.UserTimeFormatter; import com.orgzly.org.datetime.OrgDateTime; -import com.orgzly.test.BuildConfig; import org.junit.After; import org.junit.Before; diff --git a/app/src/androidTest/java/com/orgzly/android/TestUtils.java b/app/src/androidTest/java/com/orgzly/android/TestUtils.java index 07eb863a3..0da43deb3 100644 --- a/app/src/androidTest/java/com/orgzly/android/TestUtils.java +++ b/app/src/androidTest/java/com/orgzly/android/TestUtils.java @@ -46,12 +46,21 @@ public SyncRepo repoInstance(RepoType type, String url) { return dataRepository.getRepoInstance(13, type, url); } + public SyncRepo repoInstance(RepoType type, String url, Long id) { + return dataRepository.getRepoInstance(id, type, url); + } + public Repo setupRepo(RepoType type, String url) { long id = dataRepository.createRepo(new RepoWithProps(new Repo(0, type, url))); return dataRepository.getRepo(id); } + public Repo setupRepo(RepoType type, String url, Map props) { + long id = dataRepository.createRepo(new RepoWithProps(new Repo(0, type, url), props)); + return dataRepository.getRepo(id); + } + public void deleteRepo(String url) { Repo repo = dataRepository.getRepo(url); if (repo != null) { diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/SyncingTest.java b/app/src/androidTest/java/com/orgzly/android/espresso/SyncingTest.java index 8a6d65fe3..6fa4d738a 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/SyncingTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/SyncingTest.java @@ -74,29 +74,6 @@ public void testRunSync() { sync(); } - @Test - public void testForceLoadingBookWithLink() { - Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); - testUtils.setupRook(repo, "mock://repo-a/booky.org", "New content", "abc", 1234567890000L); - testUtils.setupBook("booky", "First book used for testing\n* Note A"); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("booky"), isDisplayed())).perform(longClick()); - contextualToolbarOverflowMenu().perform(click()); - onView(withText(R.string.books_context_menu_item_set_link)).perform(click()); - onView(withText("mock://repo-a")).perform(click()); - - onView(allOf(withText("booky"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_load)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - onBook(0, R.id.item_book_last_action) - .check(matches((withText(containsString(context.getString(R.string.force_loaded_from_uri, "mock://repo-a/booky.org")))))); - - onView(allOf(withText("booky"), isDisplayed())).perform(click()); - onView(allOf(withId(R.id.item_preface_text_view), withText("New content"))) - .check(matches(isDisplayed())); - } - @Test public void testAutoSyncIsTriggeredAfterCreatingNote() { Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); @@ -169,64 +146,6 @@ public void nonLinkedBookCannotBeMadeOutOfSync() { onBook(0, R.id.item_book_sync_needed_icon).check(matches(not(isDisplayed()))); } - @Test - public void testForceLoadingBookWithNoLinkNoRepos() { - testUtils.setupBook("booky", "First book used for testing\n* Note A"); - testUtils.setupBook("book-two", "Second book used for testing\n* Note 1\n* Note 2"); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("booky"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_load)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - onSnackbar().check(matches(withText(endsWith(context.getString(R.string.message_book_has_no_link))))); - } - - @Test - public void testForceLoadingBookWithNoLinkSingleRepo() { - testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); - testUtils.setupBook("booky", "First book used for testing\n* Note A"); - testUtils.setupBook("book-two", "Second book used for testing\n* Note 1\n* Note 2"); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("booky"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_load)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - onSnackbar().check(matches(withText(endsWith(context.getString(R.string.message_book_has_no_link))))); - } - - /* Books view was returning multiple entries for the same book, due to duplicates in encodings - * table. The last statement in this method will fail if there are multiple books matching. - */ - @Test - public void testForceLoadingMultipleTimes() { - Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); - testUtils.setupRook(repo, "mock://repo-a/book-one.org", "New content", "abc", 1234567890000L); - testUtils.setupBook("book-one", "First book used for testing\n* Note A"); - testUtils.setupBook("book-two", "Second book used for testing\n* Note 1\n* Note 2"); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("book-one"), isDisplayed())).perform(longClick()); - contextualToolbarOverflowMenu().perform(click()); - onView(withText(R.string.books_context_menu_item_set_link)).perform(click()); - onView(withText("mock://repo-a")).perform(click()); - - onView(allOf(withText("book-one"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_load)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - - onBook(0, R.id.item_book_last_action) - .check(matches(withText(endsWith( - context.getString(R.string.force_loaded_from_uri, "mock://repo-a/book-one.org"))))); - - onView(allOf(withText("book-one"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_load)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - - onBook(0, R.id.item_book_last_action) - .check(matches(withText(endsWith( - context.getString(R.string.force_loaded_from_uri, "mock://repo-a/book-one.org"))))); - } - /* * Book is left with out-of-sync icon when it's modified, then force-loaded. * This is because book's mtime was not being updated and was greater then remote book's mtime. @@ -296,69 +215,6 @@ public void testForceLoadingMultipleBooks() { onNoteInBook(1, R.id.item_head_title_view).check(matches(withText("Note 1"))); } - @Test - public void testForceSavingBookWithNoLinkAndMultipleRepos() { - testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); - testUtils.setupRepo(RepoType.MOCK, "mock://repo-b"); - testUtils.setupBook("book-one", "First book used for testing\n* Note A"); - testUtils.setupBook("book-two", "Second book used for testing\n* Note 1\n* Note 2"); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("book-one"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_save)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - - onBook(0, R.id.item_book_last_action) - .check(matches(withText(endsWith( - context.getString(R.string.force_saving_failed, context.getString(R.string.multiple_repos)))))); - - } - - @Test - public void testForceSavingBookWithNoLinkNoRepos() { - testUtils.setupBook("book-one", "First book used for testing\n* Note A"); - testUtils.setupBook("book-two", "Second book used for testing\n* Note 1\n* Note 2"); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("book-one"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_save)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - onBook(0, R.id.item_book_last_action) - .check(matches(withText(endsWith( - context.getString(R.string.force_saving_failed, context.getString(R.string.no_repos)))))); - } - - @Test - public void testForceSavingBookWithNoLinkSingleRepo() { - testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); - testUtils.setupBook("book-one", "First book used for testing\n* Note A"); - testUtils.setupBook("book-two", "Second book used for testing\n* Note 1\n* Note 2"); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("book-one"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_save)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - - onBook(0, R.id.item_book_last_action) - .check(matches(withText(endsWith( - context.getString(R.string.force_saved_to_uri, "mock://repo-a/book-one.org"))))); - } - - @Test - public void testForceSavingBookWithLink() { - Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); - testUtils.setupBook("booky", "First book used for testing\n* Note A", repo); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("booky"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_save)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - - onBook(0, R.id.item_book_last_action) - .check(matches(withText(endsWith( - context.getString(R.string.force_saved_to_uri, "mock://repo-a/booky.org"))))); - } - @Test public void testForceSavingMultipleBooks() { Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); @@ -784,21 +640,6 @@ public void testSettingLinkForLoadedOrgTxtBook() { onBook(0, R.id.item_book_synced_url).check(matches(allOf(withText("mock://repo-a/booky.org.txt"), isDisplayed()))); } - @Test - public void testSpaceSeparatedBookName() { - Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); - testUtils.setupRook(repo, "mock://repo-a/Book%20Name.org", "", "1abcdef", 1400067155); - - scenario = ActivityScenario.launch(MainActivity.class); - - sync(); - - onBook(0, R.id.item_book_synced_url) - .check(matches(allOf(withText("mock://repo-a/Book%20Name.org"), isDisplayed()))); - onBook(0, R.id.item_book_last_action) - .check(matches(allOf(withText(endsWith("Loaded from mock://repo-a/Book%20Name.org")), isDisplayed()))); - } - @Test public void testRenameModifiedBook() { testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java b/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java index d0546380c..3db2b9425 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java @@ -73,6 +73,7 @@ public void testLoadRook() throws IOException { assertEquals("remote-book-1", BookName.fromRook(book.getSyncedTo()).getName()); assertEquals("0abcdef", book.getSyncedTo().getRevision()); assertEquals(1400067156000L, book.getSyncedTo().getMtime()); + assertEquals(repo.getUrl(), vrook.getRepoUri().toString()); } @Test diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java b/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java index a26ae2a5c..8a56286b1 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java @@ -118,7 +118,6 @@ public void testListDownloadsDirectory() throws IOException { assertNotNull(repo.getBooks()); } - // TODO: Do the same for dropbox repo @Test public void testRenameBook() { BookView bookView; diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DocumentRepoTest.kt b/app/src/androidTest/java/com/orgzly/android/repos/DocumentRepoTest.kt new file mode 100644 index 000000000..6ee38a981 --- /dev/null +++ b/app/src/androidTest/java/com/orgzly/android/repos/DocumentRepoTest.kt @@ -0,0 +1,203 @@ +package com.orgzly.android.repos + +import android.net.Uri +import android.os.Build +import android.os.SystemClock +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector +import com.orgzly.R +import com.orgzly.android.OrgzlyTest +import com.orgzly.android.db.entity.Repo +import com.orgzly.android.espresso.util.EspressoUtils +import com.orgzly.android.ui.repos.ReposActivity +import org.hamcrest.core.AllOf +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.io.IOException + +class DocumentRepoTest : SyncRepoTest, OrgzlyTest() { + + private lateinit var documentTreeSegment: String + private lateinit var repo: Repo + private lateinit var syncRepo: SyncRepo + private lateinit var repoDirectory: DocumentFile + + @Before + override fun setUp() { + super.setUp() + setupDocumentRepo() + } + + @After + override fun tearDown() { + super.tearDown() + for (file in repoDirectory.listFiles()) { + file.delete() + } + } + + @Test + override fun testGetBooks_singleOrgFile() { + SyncRepoTest.testGetBooks_singleOrgFile(repoDirectory, syncRepo) + } + + @Test + override fun testGetBooks_singleFileInSubfolder() { + SyncRepoTest.testGetBooks_singleFileInSubfolder(repoDirectory, syncRepo) + } + + @Test + override fun testGetBooks_allFilesAreIgnored() { + SyncRepoTest.testGetBooks_allFilesAreIgnored(repoDirectory, syncRepo) + } + + @Test + override fun testGetBooks_specificFileInSubfolderIsIgnored() { + SyncRepoTest.testGetBooks_specificFileInSubfolderIsIgnored(repoDirectory, syncRepo) + } + + @Test + override fun testGetBooks_specificFileIsUnignored() { + SyncRepoTest.testGetBooks_specificFileIsUnignored(repoDirectory, syncRepo) + } + + @Test + override fun testGetBooks_ignoredExtensions() { + SyncRepoTest.testGetBooks_ignoredExtensions(repoDirectory, syncRepo) + } + + @Test + override fun testStoreBook_expectedUri() { + SyncRepoTest.testStoreBook_expectedUri(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsRetrieveBook() { + SyncRepoTest.testStoreBook_producesSameUriAsRetrieveBook(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsGetBooks() { + SyncRepoTest.testStoreBook_producesSameUriAsGetBooks(repoDirectory, syncRepo) + } + + @Test + override fun testStoreBook_inSubfolder() { + SyncRepoTest.testStoreBook_inSubfolder(repoDirectory, syncRepo) + } + + @Test + override fun testRenameBook_expectedUri() { + SyncRepoTest.testRenameBook_expectedUri(syncRepo) + } + + @Test(expected = IOException::class) + override fun testRenameBook_repoFileAlreadyExists() { + SyncRepoTest.testRenameBook_repoFileAlreadyExists(repoDirectory, syncRepo) + } + + @Test + override fun testRenameBook_fromRootToSubfolder() { + SyncRepoTest.testRenameBook_fromRootToSubfolder(syncRepo) + } + + @Test + override fun testRenameBook_fromSubfolderToRoot() { + SyncRepoTest.testRenameBook_fromSubfolderToRoot(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderSameLeafName() { + SyncRepoTest.testRenameBook_newSubfolderSameLeafName(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderAndLeafName() { + SyncRepoTest.testRenameBook_newSubfolderAndLeafName(syncRepo) + } + + @Test + override fun testRenameBook_sameSubfolderNewLeafName() { + SyncRepoTest.testRenameBook_sameSubfolderNewLeafName(syncRepo) + } + + private fun setupDocumentRepo(extraDir: String? = null) { + val repoDirName = SyncRepoTest.repoDirName + documentTreeSegment = if (Build.VERSION.SDK_INT < 30) { + "/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2F$repoDirName%2F" + } else { + "/document/primary%3A$repoDirName%2F" + } + var treeDocumentFileUrl = if (Build.VERSION.SDK_INT < 30) { + "content://com.android.providers.downloads.documents/tree/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2F$repoDirName" + } else { + "content://com.android.externalstorage.documents/tree/primary%3A$repoDirName" + } + if (extraDir != null) { + treeDocumentFileUrl = "$treeDocumentFileUrl%2F" + Uri.encode(extraDir) + } + repoDirectory = DocumentFile.fromTreeUri(context, treeDocumentFileUrl.toUri())!! + repo = if (!repoDirectory.exists()) { + if (extraDir != null) { + setupDocumentRepoInUi(extraDir) + } else { + setupDocumentRepoInUi(repoDirName) + } + dataRepository.getRepos()[0] + } else { + testUtils.setupRepo(RepoType.DOCUMENT, treeDocumentFileUrl) + } + syncRepo = testUtils.repoInstance(RepoType.DOCUMENT, repo.url, repo.id) + Assert.assertEquals(treeDocumentFileUrl, repo.url) + } + + /** + * Note that this solution only works the first time the tests are run on any given virtual + * device. On the second run, the file picker will start in a different folder, resulting in + * a different repo URL, making some tests fail. If you are running locally, you must work + * around this by wiping the device's data between test suite runs. + */ + private fun setupDocumentRepoInUi(repoDirName: String) { + ActivityScenario.launch(ReposActivity::class.java).use { + Espresso.onView(ViewMatchers.withId(R.id.activity_repos_directory)) + .perform(ViewActions.click()) + Espresso.onView(ViewMatchers.withId(R.id.activity_repo_directory_browse_button)) + .perform(ViewActions.click()) + SystemClock.sleep(500) + // In Android file browser (Espresso cannot be used): + val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + if (Build.VERSION.SDK_INT < 30) { + // Older system file picker UI + mDevice.findObject(UiSelector().description("More options")).click() + SystemClock.sleep(300) + mDevice.findObject(UiSelector().text("New folder")).click() + SystemClock.sleep(500) + mDevice.findObject(UiSelector().text("Folder name")).text = repoDirName + mDevice.findObject(UiSelector().text("OK")).click() + mDevice.findObject(UiSelector().textContains("ALLOW ACCESS TO")).click() + mDevice.findObject(UiSelector().text("ALLOW")).click() + } else { + mDevice.findObject(UiSelector().description("New folder")).click() + SystemClock.sleep(500) + mDevice.findObject(UiSelector().text("Folder name")).text = repoDirName + mDevice.findObject(UiSelector().text("OK")).click() + mDevice.findObject(UiSelector().text("USE THIS FOLDER")).click() + mDevice.findObject(UiSelector().text("ALLOW")).click() + } + // Back in Orgzly: + SystemClock.sleep(500) + Espresso.onView(ViewMatchers.isRoot()).perform(EspressoUtils.waitId(R.id.fab, 5000)) + Espresso.onView(AllOf.allOf(ViewMatchers.withId(R.id.fab), ViewMatchers.isDisplayed())) + .perform(ViewActions.click()) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java b/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java index 6a85e56a5..35d5c7c3a 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java @@ -42,112 +42,11 @@ public void testUrl() { } @Test - public void testSyncingUrlWithTrailingSlash() throws IOException { + public void testSyncingUrlWithTrailingSlash() { testUtils.setupRepo(RepoType.DROPBOX, randomUrl() + "/"); assertNotNull(testUtils.sync()); } - @Test - public void testRenameBook() throws IOException { - BookView bookView; - String repoUriString = testUtils.repoInstance(RepoType.DROPBOX, randomUrl()).getUri().toString(); - - testUtils.setupRepo(RepoType.DROPBOX, repoUriString); - testUtils.setupBook("booky", ""); - - testUtils.sync(); - bookView = dataRepository.getBookView("booky"); - - assertEquals(repoUriString, bookView.getLinkRepo().getUrl()); - assertEquals(repoUriString, bookView.getSyncedTo().getRepoUri().toString()); - assertEquals(repoUriString + "/booky.org", bookView.getSyncedTo().getUri().toString()); - - dataRepository.renameBook(bookView, "booky-renamed"); - bookView = dataRepository.getBookView("booky-renamed"); - - assertEquals(repoUriString, bookView.getLinkRepo().getUrl()); - assertEquals(repoUriString, bookView.getSyncedTo().getRepoUri().toString()); - assertEquals(repoUriString + "/booky-renamed.org", bookView.getSyncedTo().getUri().toString()); - } - - @Test - public void testIgnoreRulePreventsLinkingBook() throws Exception { - Uri repoUri = testUtils.repoInstance(RepoType.DROPBOX, randomUrl()).getUri(); - testUtils.setupRepo(RepoType.DROPBOX, repoUri.toString()); - uploadFileToRepo(repoUri, RepoIgnoreNode.IGNORE_FILE, "*.org"); - testUtils.setupBook("booky", ""); - exceptionRule.expect(IOException.class); - exceptionRule.expectMessage("matches a rule in .orgzlyignore"); - testUtils.syncOrThrow(); - } - - @Test - public void testIgnoreRulePreventsLoadingBook() throws Exception { - SyncRepo repo = testUtils.repoInstance(RepoType.DROPBOX, randomUrl()); - testUtils.setupRepo(RepoType.DROPBOX, repo.getUri().toString()); - - // Create two .org files - uploadFileToRepo(repo.getUri(), "ignored.org", "1 2 3"); - uploadFileToRepo(repo.getUri(), "notignored.org", "1 2 3"); - // Create .orgzlyignore - uploadFileToRepo(repo.getUri(), RepoIgnoreNode.IGNORE_FILE, "ignored.org"); - testUtils.sync(); - - List bookViews = dataRepository.getBooks(); - assertEquals(1, bookViews.size()); - assertEquals("notignored", bookViews.get(0).getBook().getName()); - } - - @Test - public void testIgnoreRulePreventsRenamingBook() throws Exception { - BookView bookView; - Uri repoUri = testUtils.repoInstance(RepoType.DROPBOX, randomUrl()).getUri(); - testUtils.setupRepo(RepoType.DROPBOX, repoUri.toString()); - uploadFileToRepo(repoUri, RepoIgnoreNode.IGNORE_FILE, "badname*"); - testUtils.setupBook("goodname", ""); - testUtils.sync(); - bookView = dataRepository.getBookView("goodname"); - dataRepository.renameBook(bookView, "badname"); - bookView = dataRepository.getBooks().get(0); - assertTrue( - bookView.getBook() - .getLastAction() - .toString() - .contains("matches a rule in .orgzlyignore") - ); - } - - @Test - public void testDropboxFileRename() throws IOException { - SyncRepo repo = testUtils.repoInstance(RepoType.DROPBOX, randomUrl()); - - assertNotNull(repo); - assertEquals(0, repo.getBooks().size()); - - File file = File.createTempFile("notebook.", ".org"); - MiscUtils.writeStringToFile("1 2 3", file); - - VersionedRook vrook = repo.storeBook(file, file.getName()); - - file.delete(); - - assertEquals(1, repo.getBooks().size()); - - repo.renameBook(vrook.getUri(), "notebook-renamed"); - - assertEquals(1, repo.getBooks().size()); - assertEquals(repo.getUri() + "/notebook-renamed.org", repo.getBooks().get(0).getUri().toString()); - assertEquals("notebook-renamed.org", BookName.fromRook(repo.getBooks().get(0)).getRepoRelativePath()); - } - - private void uploadFileToRepo(Uri repoUri, String fileName, String fileContents) throws IOException { - DropboxClient client = new DropboxClient(App.getAppContext(), 0); - File tmpFile = File.createTempFile("abc", null); - MiscUtils.writeStringToFile(fileContents, tmpFile); - client.upload(tmpFile, repoUri, fileName); - tmpFile.delete(); - } - private String randomUrl() { return "dropbox:"+ DROPBOX_TEST_DIR + "/" + UUID.randomUUID().toString(); } diff --git a/app/src/androidTest/java/com/orgzly/android/repos/GitRepoTest.kt b/app/src/androidTest/java/com/orgzly/android/repos/GitRepoTest.kt deleted file mode 100644 index 3f4b1a260..000000000 --- a/app/src/androidTest/java/com/orgzly/android/repos/GitRepoTest.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.orgzly.android.repos - -import android.net.Uri -import android.os.Build -import androidx.core.net.toUri -import com.orgzly.R -import com.orgzly.android.OrgzlyTest -import com.orgzly.android.db.entity.BookView -import com.orgzly.android.db.entity.Repo -import com.orgzly.android.git.GitFileSynchronizer -import com.orgzly.android.git.GitPreferencesFromRepoPrefs -import com.orgzly.android.prefs.AppPreferences -import com.orgzly.android.prefs.RepoPreferences -import com.orgzly.android.util.MiscUtils -import org.eclipse.jgit.api.Git -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Assume -import org.junit.Rule -import org.junit.Test -import org.junit.rules.ExpectedException -import java.io.File -import java.io.IOException -import java.nio.file.Path -import kotlin.io.path.createTempDirectory - -class GitRepoTest : OrgzlyTest() { - - private lateinit var bareRepoPath: Path - private lateinit var repoUri: Uri - private lateinit var gitPreferences: GitPreferencesFromRepoPrefs - private lateinit var workingtree: File - private lateinit var repoPreferences: RepoPreferences - private lateinit var repo: Repo - private lateinit var syncRepo: GitRepo - private lateinit var git: Git - private lateinit var synchronizer: GitFileSynchronizer - - @Rule - @JvmField - var exceptionRule: ExpectedException = ExpectedException.none() - - override fun setUp() { - super.setUp() - bareRepoPath = createTempDirectory() - Git.init().setBare(true).setDirectory(bareRepoPath.toFile()).call() - AppPreferences.gitIsEnabled(context, true) - repoUri = bareRepoPath.toFile().toUri() - repo = testUtils.setupRepo(RepoType.GIT, repoUri.toString()) - repoPreferences = RepoPreferences(context, repo.id, repoUri) - gitPreferences = GitPreferencesFromRepoPrefs(repoPreferences) - workingtree = File(gitPreferences.repositoryFilepath()) - workingtree.mkdirs() - git = GitRepo.ensureRepositoryExists(gitPreferences, true, null) - syncRepo = dataRepository.getRepoInstance(repo.id, RepoType.GIT, repo.url) as GitRepo - synchronizer = GitFileSynchronizer(git, gitPreferences) - } - - override fun tearDown() { - super.tearDown() - testUtils.deleteRepo(repo.url) - workingtree.deleteRecursively() - bareRepoPath.toFile()?.deleteRecursively() - } - - @Test - fun testSyncNewBookWithoutLinkAndOneRepo() { - testUtils.setupBook("book1", "book content") - testUtils.sync() - val bookView = dataRepository.getBooks()[0] - assertEquals(repoUri.toString(), bookView.linkRepo?.url) - assertEquals(1, syncRepo.books.size) - assertEquals(bookView.syncedTo.toString(), syncRepo.books[0].toString()) - assertEquals(context.getString(R.string.sync_status_saved, repo.url), bookView.book.lastAction!!.message) - assertEquals("/book1.org", bookView.syncedTo!!.uri.toString()) - } - - @Test - fun testIgnoredFilesInRepoAreNotLoaded() { - Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - // Create ignore file in working tree and commit - val ignoreFileContents = """ - ignoredbook.org - ignored-*.org - """.trimIndent() - addAndCommitIgnoreFile(ignoreFileContents) - // Add multiple files to repo - for (fileName in arrayOf("ignoredbook.org", "ignored-3.org", "notignored.org")) { - val tmpFile = File.createTempFile("orgzlytest", null) - MiscUtils.writeStringToFile("book content", tmpFile) - synchronizer.addAndCommitNewFile(tmpFile, fileName) - tmpFile.delete() - } - testUtils.sync() - assertEquals(1, syncRepo.books.size) - assertEquals(1, dataRepository.getBooks().size) - assertEquals("notignored", dataRepository.getBooks()[0].book.name) - } - - @Test - fun testUnIgnoredFilesInRepoAreLoaded() { - Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - // Create ignore file in working tree and commit - val ignoreFileContents = """ - *.org - !notignored.org - """.trimIndent() - addAndCommitIgnoreFile(ignoreFileContents) - // Add multiple files to repo - for (fileName in arrayOf("ignoredbook.org", "ignored-3.org", "notignored.org")) { - val tmpFile = File.createTempFile("orgzlytest", null) - MiscUtils.writeStringToFile("book content", tmpFile) - synchronizer.addAndCommitNewFile(tmpFile, fileName) - tmpFile.delete() - } - testUtils.sync() - assertEquals(1, syncRepo.books.size) - assertEquals(1, dataRepository.getBooks().size) - assertEquals("notignored", dataRepository.getBooks()[0].book.name) - } - - @Test - fun testIgnoreRulePreventsLinkingBook() { - Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - addAndCommitIgnoreFile("*.org") - testUtils.setupBook("booky", "") - exceptionRule.expect(IOException::class.java) - exceptionRule.expectMessage("matches a rule in .orgzlyignore") - testUtils.syncOrThrow() - } - - @Test - fun testIgnoreRulePreventsRenamingBook() { - Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - addAndCommitIgnoreFile("badname*") - testUtils.setupBook("goodname", "") - testUtils.sync() - var bookView: BookView? = dataRepository.getBookView("goodname") - dataRepository.renameBook(bookView!!, "badname") - bookView = dataRepository.getBooks()[0] - assertTrue( - bookView.book.lastAction.toString().contains("matches a rule in .orgzlyignore") - ) - } - - private fun addAndCommitIgnoreFile(contents: String) { - val tmpFile = File.createTempFile("orgzlytest", null) - MiscUtils.writeStringToFile(contents, tmpFile) - synchronizer.addAndCommitNewFile(tmpFile, RepoIgnoreNode.IGNORE_FILE) - tmpFile.delete() - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java b/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java index 31fa1aaca..2fc5f4cac 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java @@ -1,7 +1,24 @@ package com.orgzly.android.repos; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.orgzly.android.espresso.util.EspressoUtils.onBook; +import static org.hamcrest.Matchers.allOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + import android.net.Uri; +import android.os.Build; +import com.orgzly.R; +import com.orgzly.android.BookFormat; import com.orgzly.android.BookName; import com.orgzly.android.LocalStorage; import com.orgzly.android.OrgzlyTest; @@ -15,21 +32,17 @@ import com.orgzly.android.util.MiscUtils; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import java.io.File; import java.io.IOException; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - public class SyncTest extends OrgzlyTest { private static final String TAG = SyncTest.class.getName(); @@ -38,6 +51,9 @@ public void setUp() throws Exception { super.setUp(); } + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + @Test public void testOrgRange() { Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); @@ -226,13 +242,19 @@ public void testOnlyBookWithLink() { } @Test - public void testOnlyBookWithoutLinkAndOneRepo() { - testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + public void testOnlyBookWithoutLinkAndOneRepo() throws IOException { + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); testUtils.setupBook("book-1", "Content"); testUtils.sync(); BookView book = dataRepository.getBooks().get(0); assertEquals(BookSyncStatus.ONLY_BOOK_WITHOUT_LINK_AND_ONE_REPO.toString(), book.getBook().getSyncStatus()); + assertEquals(context.getString(R.string.sync_status_saved, repo.getUrl()), + book.getBook().getLastAction().getMessage()); + assertEquals(repo.getUrl(), book.getLinkRepo().getUrl()); + SyncRepo syncRepo = testUtils.repoInstance(RepoType.MOCK, repo.getUrl()); + assertEquals(1, syncRepo.getBooks().size()); + assertEquals(syncRepo.getBooks().get(0).toString(), book.getSyncedTo().toString()); } @Test @@ -437,7 +459,7 @@ public void testDirectoryFileRename() throws IOException { } @Test - public void testRenameSyncedBook() { + public void testRenameSyncedBook() throws IOException { testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); testUtils.setupBook("Booky", "1 2 3"); @@ -455,6 +477,30 @@ public void testRenameSyncedBook() { assertEquals("mock://repo-a", renamedBook.getLinkRepo().getUrl()); assertEquals("mock://repo-a", renamedBook.getSyncedTo().getRepoUri().toString()); assertEquals("mock://repo-a/BookyRenamed.org", renamedBook.getSyncedTo().getUri().toString()); + assertEquals("1 2 3\n\n", dataRepository.getBookContent("BookyRenamed", BookFormat.ORG)); + } + + @Test + public void testRenameBookToNameWithSpace() throws IOException { + testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + testUtils.setupBook("Booky", "1 2 3"); + + testUtils.sync(); + + BookView book = dataRepository.getBookView("Booky"); + + assertEquals("mock://repo-a/Booky.org", book.getSyncedTo().getUri().toString()); + + dataRepository.renameBook(book, "Booky Renamed"); + + BookView renamedBook = dataRepository.getBookView("Booky Renamed"); + + assertNotNull(renamedBook); + assertEquals("mock://repo-a", renamedBook.getLinkRepo().getUrl()); + assertEquals("mock://repo-a", renamedBook.getSyncedTo().getRepoUri().toString()); + assertEquals("mock://repo-a/Booky%20Renamed.org", + renamedBook.getSyncedTo().getUri().toString()); + assertEquals("1 2 3\n\n", dataRepository.getBookContent("Booky Renamed", BookFormat.ORG)); } @Test @@ -490,6 +536,84 @@ public void testRenameSyncedBookWithDifferentLink() throws IOException { assertEquals("mock://repo-a/Booky.org", book.getSyncedTo().getUri().toString()); } + @Test + public void testRenameBookToExistingBookName() { + testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + testUtils.setupBook("a", ""); + testUtils.setupBook("b", ""); + assertEquals(2, dataRepository.getBooks().size()); + dataRepository.renameBook(dataRepository.getBookView("a"), "b"); + assertTrue(dataRepository.getBook("a") + .getLastAction() + .getMessage() + .contains("Renaming failed: Notebook b already exists") + ); + } + + @Test + public void testIgnoreRulePreventsRenamingBook() { + assumeTrue(Build.VERSION.SDK_INT >= 26); + String ignoreRules = "bad name*\n"; + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + + // Add ignore rules using repo properties (N.B. MockRepo-specific solution) + Map repoPropsMap = new HashMap<>(); + repoPropsMap.put(MockRepo.IGNORE_RULES_PREF_KEY, ignoreRules); + RepoWithProps repoWithProps = new RepoWithProps(repo, repoPropsMap); + dataRepository.updateRepo(repoWithProps); + + // Create book and sync it + testUtils.setupBook("good name", ""); + testUtils.sync(); + BookView bookView = dataRepository.getBookView("good name"); + + dataRepository.renameBook(bookView, "bad name"); + bookView = dataRepository.getBooks().get(0); + assertTrue(bookView.getBook() + .getLastAction() + .toString() + .contains("matches a rule in .orgzlyignore")); + } + + @Test + public void testIgnoreRulePreventsLinkingBook() throws Exception { + assumeTrue(Build.VERSION.SDK_INT >= 26); + String ignoreRules = "*.org\n"; + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + + // Add ignore rules using repo properties (N.B. MockRepo-specific solution) + Map repoPropsMap = new HashMap<>(); + repoPropsMap.put(MockRepo.IGNORE_RULES_PREF_KEY, ignoreRules); + RepoWithProps repoWithProps = new RepoWithProps(repo, repoPropsMap); + dataRepository.updateRepo(repoWithProps); + + // Create book and sync it + testUtils.setupBook("booky", ""); + exceptionRule.expect(IOException.class); + exceptionRule.expectMessage("matches a rule in .orgzlyignore"); + testUtils.syncOrThrow(); + } + + + /** + * Ensures that file names and book names are not parsed/created differently during + * force-loading. + */ + @Test + public void testForceLoadBookInSubfolder() { + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + BookView bookView = testUtils.setupBook("a folder/a book", "content"); + testUtils.sync(); + var books = dataRepository.getBooks(); + assertEquals(1, books.size()); + assertEquals("a folder/a book", books.get(0).getBook().getName()); + dataRepository.forceLoadBook(bookView.getBook().getId()); + books = dataRepository.getBooks(); + assertEquals(1, books.size()); + // Check that the name has not changed + assertEquals("a folder/a book", books.get(0).getBook().getName()); + } + /** * We remove the local book's' syncedTo attribute and repository link when its remote file * has been deleted, to make it easier to ascertain the book's state during subsequent sync @@ -533,4 +657,126 @@ public void testBookStatusAfterMultipleSyncsFollowingRemoteFileDeletion() throws assertNull(book.getLinkRepo()); assertEquals(BookSyncStatus.BOOK_WITH_PREVIOUS_ERROR_AND_NO_LINK.toString(), book.getBook().getSyncStatus()); } + + @Test + public void testSpaceSeparatedBookName() { + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + testUtils.setupRook(repo, "mock://repo-a/Book%20Name.org", "", "1abcdef", 1400067155); + + testUtils.sync(); + + BookView bookView = dataRepository.getBooks().get(0); + assertNotNull(bookView.getSyncedTo()); + assertEquals("Book Name", bookView.getBook().getName()); + assertEquals("mock://repo-a/Book%20Name.org", bookView.getSyncedTo().getUri().toString()); + assertEquals("Loaded from mock://repo-a/Book%20Name.org", + bookView.getBook().getLastAction().getMessage()); + } + + @Test + public void testForceLoadingBookWithLink() throws IOException { + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + testUtils.setupRook(repo, "mock://repo-a/booky.org", "New content", "abc", 1234567890000L); + Book book = testUtils.setupBook("booky", "First book used for testing\n* Note A").getBook(); + dataRepository.setLink(book.getId(), repo); + dataRepository.forceLoadBook(book.getId()); + + assertEquals(context.getString(R.string.force_loaded_from_uri, "mock://repo-a/booky.org") + , dataRepository.getBook(book.getName()).getLastAction().getMessage()); + assertEquals("New content\n\n", dataRepository.getBookContent("booky", BookFormat.ORG)); + } + + /** + * To ensure that book names are not parsed/constructed differently during force load + */ + @Test + public void testForceLoadBookWithSpaceInName() { + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + testUtils.setupRook(repo, "mock://repo-a/Book%20Name.org", "", "1abcdef", 1400067155); + + testUtils.sync(); + + BookView bookView = dataRepository.getBooks().get(0); + assertEquals("Book Name", bookView.getBook().getName()); + + dataRepository.forceLoadBook(bookView.getBook().getId()); + assertEquals("Book Name", dataRepository.getBooks().get(0).getBook().getName()); + } + + @Test + public void testForceLoadingBookWithNoLinkNoRepos() { + BookView book = testUtils.setupBook("booky", "First book used for testing\n* Note A"); + + exceptionRule.expect(IOException.class); + exceptionRule.expectMessage(context.getString(R.string.message_book_has_no_link)); + dataRepository.forceLoadBook(book.getBook().getId()); + } + + @Test + public void testForceLoadingBookWithNoLinkSingleRepo() { + testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + BookView book = testUtils.setupBook("booky", "First book used for testing\n* Note A"); + + exceptionRule.expect(IOException.class); + exceptionRule.expectMessage(context.getString(R.string.message_book_has_no_link)); + dataRepository.forceLoadBook(book.getBook().getId()); + } + + /* Books view was returning multiple entries for the same book, due to duplicates in encodings + * table. The last statement in this method will fail if there are multiple books matching. + */ + @Test + public void testForceLoadingMultipleTimes() { + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + testUtils.setupRook(repo, "mock://repo-a/book-one.org", "New content", "abc", 1234567890000L); + Book book = testUtils.setupBook("book-one", "First book used for testing\n* Note A").getBook(); + dataRepository.setLink(book.getId(), repo); + dataRepository.forceLoadBook(book.getId()); + assertEquals( + context.getString(R.string.force_loaded_from_uri, "mock://repo-a/book-one.org"), + dataRepository.getBook(book.getId()).getLastAction().getMessage() + ); + dataRepository.forceLoadBook(book.getId()); + assertEquals( + context.getString(R.string.force_loaded_from_uri, "mock://repo-a/book-one.org"), + dataRepository.getBook(book.getId()).getLastAction().getMessage() + ); + } + + @Test + public void testForceSavingBookWithNoLinkAndMultipleRepos() { + testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + testUtils.setupRepo(RepoType.MOCK, "mock://repo-b"); + Book book = testUtils.setupBook("book-one", "First book used for testing\n* Note A").getBook(); + exceptionRule.expect(IOException.class); + exceptionRule.expectMessage(context.getString(R.string.force_saving_failed, context.getString(R.string.multiple_repos))); + dataRepository.forceSaveBook(book.getId()); + } + + @Test + public void testForceSavingBookWithNoLinkNoRepos() { + Book book = testUtils.setupBook("book-one", "First book used for testing\n* Note A").getBook(); + exceptionRule.expect(IOException.class); + exceptionRule.expectMessage(context.getString(R.string.force_saving_failed, context.getString(R.string.no_repos))); + dataRepository.forceSaveBook(book.getId()); + } + + @Test + public void testForceSavingBookWithNoLinkSingleRepo() { + testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + Book book = testUtils.setupBook("book-one", "First book used for testing\n* Note A").getBook(); + dataRepository.forceSaveBook(book.getId()); + assertEquals(context.getString(R.string.force_saved_to_uri, "mock://repo-a/book-one.org") + , dataRepository.getBook(book.getId()).getLastAction().getMessage()); + } + + @Test + public void testForceSavingBookWithLink() { + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + Book book = testUtils.setupBook("booky", "First book used for testing\n* Note A", repo).getBook(); + dataRepository.setLink(book.getId(), repo); + dataRepository.forceSaveBook(book.getId()); + assertEquals(context.getString(R.string.force_saved_to_uri, "mock://repo-a/booky.org") + , dataRepository.getBook(book.getId()).getLastAction().getMessage()); + } } diff --git a/app/src/test/java/com/orgzly/android/repos/DropboxRepoTest.kt b/app/src/test/java/com/orgzly/android/repos/DropboxRepoTest.kt new file mode 100644 index 000000000..1b49db0b4 --- /dev/null +++ b/app/src/test/java/com/orgzly/android/repos/DropboxRepoTest.kt @@ -0,0 +1,135 @@ +package com.orgzly.android.repos + +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.orgzly.BuildConfig +import com.orgzly.android.db.entity.Repo +import com.orgzly.android.prefs.AppPreferences +import org.json.JSONObject +import org.junit.After +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException +import java.util.UUID + +@RunWith(AndroidJUnit4::class) +class DropboxRepoTest : SyncRepoTest { + + private lateinit var syncRepo: SyncRepo + private lateinit var client: DropboxClient + + @Before + fun setup() { + assumeTrue(BuildConfig.DROPBOX_APP_KEY.isNotEmpty()) + assumeTrue(BuildConfig.DROPBOX_REFRESH_TOKEN.isNotEmpty()) + val mockSerializedDbxCredential = JSONObject() + mockSerializedDbxCredential.put("access_token", "dummy") + mockSerializedDbxCredential.put("expires_at", System.currentTimeMillis()) + mockSerializedDbxCredential.put("refresh_token", BuildConfig.DROPBOX_REFRESH_TOKEN) + mockSerializedDbxCredential.put("app_key", BuildConfig.DROPBOX_APP_KEY) + AppPreferences.dropboxSerializedCredential( + ApplicationProvider.getApplicationContext(), + mockSerializedDbxCredential.toString() + ) + val repo = Repo(0, RepoType.DROPBOX, "dropbox:/${SyncRepoTest.repoDirName}/" + UUID.randomUUID().toString()) + val repoPropsMap = HashMap() + val repoWithProps = RepoWithProps(repo, repoPropsMap) + syncRepo = DropboxRepo(repoWithProps, ApplicationProvider.getApplicationContext()) + client = DropboxClient(ApplicationProvider.getApplicationContext(), repo.id) + } + + @After + fun tearDown() { + if (this::syncRepo.isInitialized) { + val dropboxRepo = syncRepo as DropboxRepo + dropboxRepo.deleteDirectory(syncRepo.uri) + } + } + + @Test + override fun testGetBooks_singleOrgFile() { + SyncRepoTest.testGetBooks_singleOrgFile(client, syncRepo) + } + + @Test + override fun testGetBooks_singleFileInSubfolder() { + SyncRepoTest.testGetBooks_singleFileInSubfolder(client, syncRepo) + } + + @Test + override fun testGetBooks_allFilesAreIgnored() { + SyncRepoTest.testGetBooks_allFilesAreIgnored(client, syncRepo) + } + + @Test + override fun testGetBooks_specificFileInSubfolderIsIgnored() { + SyncRepoTest.testGetBooks_specificFileInSubfolderIsIgnored(client, syncRepo) + } + + @Test + override fun testGetBooks_specificFileIsUnignored() { + SyncRepoTest.testGetBooks_specificFileIsUnignored(client, syncRepo) + } + + @Test + override fun testGetBooks_ignoredExtensions() { + SyncRepoTest.testGetBooks_ignoredExtensions(client, syncRepo) + } + + @Test + override fun testStoreBook_expectedUri() { + SyncRepoTest.testStoreBook_expectedUri(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsRetrieveBook() { + SyncRepoTest.testStoreBook_producesSameUriAsRetrieveBook(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsGetBooks() { + SyncRepoTest.testStoreBook_producesSameUriAsGetBooks(client, syncRepo) + } + + @Test + override fun testStoreBook_inSubfolder() { + SyncRepoTest.testStoreBook_inSubfolder(client, syncRepo) + } + + @Test + override fun testRenameBook_expectedUri() { + SyncRepoTest.testRenameBook_expectedUri(syncRepo) + } + + @Test(expected = IOException::class) + override fun testRenameBook_repoFileAlreadyExists() { + SyncRepoTest.testRenameBook_repoFileAlreadyExists(client, syncRepo) + } + + @Test + override fun testRenameBook_fromRootToSubfolder() { + SyncRepoTest.testRenameBook_fromRootToSubfolder(syncRepo) + } + + @Test + override fun testRenameBook_fromSubfolderToRoot() { + SyncRepoTest.testRenameBook_fromSubfolderToRoot(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderSameLeafName() { + SyncRepoTest.testRenameBook_newSubfolderSameLeafName(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderAndLeafName() { + SyncRepoTest.testRenameBook_newSubfolderAndLeafName(syncRepo) + } + + @Test + override fun testRenameBook_sameSubfolderNewLeafName() { + SyncRepoTest.testRenameBook_sameSubfolderNewLeafName(syncRepo) + } +} diff --git a/app/src/test/java/com/orgzly/android/repos/GitRepoTest.kt b/app/src/test/java/com/orgzly/android/repos/GitRepoTest.kt new file mode 100644 index 000000000..883be5b03 --- /dev/null +++ b/app/src/test/java/com/orgzly/android/repos/GitRepoTest.kt @@ -0,0 +1,137 @@ +package com.orgzly.android.repos + +import android.content.Context +import androidx.core.net.toUri +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.orgzly.android.db.entity.Repo +import com.orgzly.android.git.GitFileSynchronizer +import com.orgzly.android.git.GitPreferencesFromRepoPrefs +import com.orgzly.android.prefs.AppPreferences +import com.orgzly.android.prefs.RepoPreferences +import org.eclipse.jgit.api.Git +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File +import java.io.IOException +import kotlin.io.path.createTempDirectory + +@RunWith(AndroidJUnit4::class) +class GitRepoTest : SyncRepoTest { + + private lateinit var gitWorkingTree: File + private lateinit var bareRepoDir: File + private lateinit var gitFileSynchronizer: GitFileSynchronizer + private lateinit var syncRepo: SyncRepo + private val context: Context = ApplicationProvider.getApplicationContext() + + @Before + fun setup() { + bareRepoDir = createTempDirectory().toFile() + Git.init().setBare(true).setDirectory(bareRepoDir).call() + AppPreferences.gitIsEnabled(context, true) + val repo = Repo(0, RepoType.GIT, "file://$bareRepoDir") + val repoPreferences = RepoPreferences(context, repo.id, repo.url.toUri()) + val gitPreferences = GitPreferencesFromRepoPrefs(repoPreferences) + gitWorkingTree = File(gitPreferences.repositoryFilepath()) + gitWorkingTree.mkdirs() + val git = GitRepo.ensureRepositoryExists(gitPreferences, true, null) + gitFileSynchronizer = GitFileSynchronizer(git, gitPreferences) + val repoPropsMap = HashMap() + val repoWithProps = RepoWithProps(repo, repoPropsMap) + syncRepo = GitRepo.getInstance(repoWithProps, context) + } + + @After + fun tearDown() { + gitWorkingTree.deleteRecursively() + bareRepoDir.deleteRecursively() + } + + @Test + override fun testGetBooks_singleOrgFile() { + SyncRepoTest.testGetBooks_singleOrgFile(gitWorkingTree, syncRepo) + } + + @Test + override fun testGetBooks_singleFileInSubfolder() { + SyncRepoTest.testGetBooks_singleFileInSubfolder(gitWorkingTree, syncRepo) + } + + @Test + override fun testGetBooks_allFilesAreIgnored() { + SyncRepoTest.testGetBooks_allFilesAreIgnored(gitWorkingTree, syncRepo) + } + + @Test + override fun testGetBooks_specificFileInSubfolderIsIgnored() { + SyncRepoTest.testGetBooks_specificFileInSubfolderIsIgnored(gitWorkingTree, syncRepo) + } + + @Test + override fun testGetBooks_specificFileIsUnignored() { + SyncRepoTest.testGetBooks_specificFileIsUnignored(gitWorkingTree, syncRepo) + } + + @Test + override fun testGetBooks_ignoredExtensions() { + SyncRepoTest.testGetBooks_ignoredExtensions(gitWorkingTree, syncRepo) + } + + @Test + override fun testStoreBook_expectedUri() { + SyncRepoTest.testStoreBook_expectedUri(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsRetrieveBook() { + SyncRepoTest.testStoreBook_producesSameUriAsRetrieveBook(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsGetBooks() { + SyncRepoTest.testStoreBook_producesSameUriAsGetBooks(gitWorkingTree, syncRepo) + } + + @Test + override fun testStoreBook_inSubfolder() { + SyncRepoTest.testStoreBook_inSubfolder(gitWorkingTree, syncRepo) + } + + @Test + override fun testRenameBook_expectedUri() { + SyncRepoTest.testRenameBook_expectedUri(syncRepo) + } + + @Test(expected = IOException::class) + override fun testRenameBook_repoFileAlreadyExists() { + SyncRepoTest.testRenameBook_repoFileAlreadyExists(gitWorkingTree, syncRepo) + } + + @Test + override fun testRenameBook_fromRootToSubfolder() { + SyncRepoTest.testRenameBook_fromRootToSubfolder(syncRepo) + } + + @Test + override fun testRenameBook_fromSubfolderToRoot() { + SyncRepoTest.testRenameBook_fromSubfolderToRoot(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderSameLeafName() { + SyncRepoTest.testRenameBook_newSubfolderSameLeafName(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderAndLeafName() { + SyncRepoTest.testRenameBook_newSubfolderAndLeafName(syncRepo) + } + + @Test + override fun testRenameBook_sameSubfolderNewLeafName() { + SyncRepoTest.testRenameBook_sameSubfolderNewLeafName(syncRepo) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/orgzly/android/repos/WebdavRepoTest.kt b/app/src/test/java/com/orgzly/android/repos/WebdavRepoTest.kt new file mode 100644 index 000000000..273e1b752 --- /dev/null +++ b/app/src/test/java/com/orgzly/android/repos/WebdavRepoTest.kt @@ -0,0 +1,138 @@ +package com.orgzly.android.repos + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.orgzly.android.db.entity.Repo +import com.orgzly.android.repos.WebdavRepo.Companion.PASSWORD_PREF_KEY +import com.orgzly.android.repos.WebdavRepo.Companion.USERNAME_PREF_KEY +import io.github.atetzner.webdav.server.MiltonWebDAVFileServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File +import java.io.IOException + + +@RunWith(AndroidJUnit4::class) +class WebdavRepoTest : SyncRepoTest { + + private val serverUrl = "http://localhost:8081" + + private lateinit var serverRootDir: File + private lateinit var localServer: MiltonWebDAVFileServer + private lateinit var syncRepo: SyncRepo + private lateinit var tmpFile: File + + @Before + fun setup() { + serverRootDir = java.nio.file.Files.createTempDirectory("orgzly-webdav-test-").toFile() + localServer = MiltonWebDAVFileServer(serverRootDir) + localServer.userCredentials["user"] = "secret" + localServer.start() + val repo = Repo(0, RepoType.WEBDAV, serverUrl) + val repoPropsMap = HashMap() + repoPropsMap[USERNAME_PREF_KEY] = "user" + repoPropsMap[PASSWORD_PREF_KEY] = "secret" + val repoWithProps = RepoWithProps(repo, repoPropsMap) + syncRepo = WebdavRepo.getInstance(repoWithProps) + assertEquals(serverUrl, repo.url) + tmpFile = kotlin.io.path.createTempFile().toFile() + } + + @After + fun tearDown() { + tmpFile.delete() + if (this::localServer.isInitialized) { + localServer.stop() + } + if (this::serverRootDir.isInitialized) { + serverRootDir.deleteRecursively() + } + } + + @Test + override fun testGetBooks_singleOrgFile() { + SyncRepoTest.testGetBooks_singleOrgFile(serverRootDir, syncRepo) + } + + @Test + override fun testGetBooks_singleFileInSubfolder() { + SyncRepoTest.testGetBooks_singleFileInSubfolder(serverRootDir, syncRepo) + } + + @Test + override fun testGetBooks_allFilesAreIgnored() { + SyncRepoTest.testGetBooks_allFilesAreIgnored(serverRootDir, syncRepo) + } + + @Test + override fun testGetBooks_specificFileInSubfolderIsIgnored() { + SyncRepoTest.testGetBooks_specificFileInSubfolderIsIgnored(serverRootDir, syncRepo) + } + + @Test + override fun testGetBooks_specificFileIsUnignored() { + SyncRepoTest.testGetBooks_specificFileIsUnignored(serverRootDir, syncRepo) + } + + @Test + override fun testGetBooks_ignoredExtensions() { + SyncRepoTest.testGetBooks_ignoredExtensions(serverRootDir, syncRepo) + } + + @Test + override fun testStoreBook_expectedUri() { + SyncRepoTest.testStoreBook_expectedUri(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsRetrieveBook() { + SyncRepoTest.testStoreBook_producesSameUriAsRetrieveBook(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsGetBooks() { + SyncRepoTest.testStoreBook_producesSameUriAsGetBooks(serverRootDir, syncRepo) + } + + @Test + override fun testStoreBook_inSubfolder() { + SyncRepoTest.testStoreBook_inSubfolder(serverRootDir, syncRepo) + } + + @Test + override fun testRenameBook_expectedUri() { + SyncRepoTest.testRenameBook_expectedUri(syncRepo) + } + + @Test(expected = IOException::class) + override fun testRenameBook_repoFileAlreadyExists() { + SyncRepoTest.testRenameBook_repoFileAlreadyExists(serverRootDir, syncRepo) + } + + @Test + override fun testRenameBook_fromRootToSubfolder() { + SyncRepoTest.testRenameBook_fromRootToSubfolder(syncRepo) + } + + @Test + override fun testRenameBook_fromSubfolderToRoot() { + SyncRepoTest.testRenameBook_fromSubfolderToRoot(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderSameLeafName() { + SyncRepoTest.testRenameBook_newSubfolderSameLeafName(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderAndLeafName() { + SyncRepoTest.testRenameBook_newSubfolderAndLeafName(syncRepo) + } + + @Test + override fun testRenameBook_sameSubfolderNewLeafName() { + SyncRepoTest.testRenameBook_sameSubfolderNewLeafName(syncRepo) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 2a35ddd25..6af49b8d1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -34,3 +34,4 @@ if (gradle.ext.appProperties.org_java_directory?.trim()) { } include ':app' +include ':shared-test' diff --git a/shared-test/.gitignore b/shared-test/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/shared-test/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/shared-test/build.gradle b/shared-test/build.gradle new file mode 100644 index 000000000..f933f5514 --- /dev/null +++ b/shared-test/build.gradle @@ -0,0 +1,51 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +android { + namespace = "com.orgzly.shared.test" + compileSdk 33 + + defaultConfig { + minSdk 21 + buildConfigField "String", "DROPBOX_APP_KEY", gradle.ext.appProperties.getProperty("dropbox.app_key", '""') + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + buildConfigField "String", "DROPBOX_REFRESH_TOKEN", gradle.ext.appProperties.getProperty("dropbox.refresh_token", '""') + } + } + flavorDimensions "store" + productFlavors { + premium { + dimension "store" + } + + fdroid { + dimension "store" + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = 11 + } + + packagingOptions { + resources.merges.add("plugin.properties") + } +} + +dependencies { + implementation project(":app") + implementation "junit:junit:$versions.junit" + implementation 'androidx.documentfile:documentfile:1.0.1' + implementation "org.eclipse.jgit:org.eclipse.jgit:$versions.jgit" +} diff --git a/shared-test/proguard-rules.pro b/shared-test/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/shared-test/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/shared-test/src/main/AndroidManifest.xml b/shared-test/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a522a4c23 --- /dev/null +++ b/shared-test/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/shared-test/src/main/java/com/orgzly/android/repos/SyncRepoTest.kt b/shared-test/src/main/java/com/orgzly/android/repos/SyncRepoTest.kt new file mode 100644 index 000000000..385910575 --- /dev/null +++ b/shared-test/src/main/java/com/orgzly/android/repos/SyncRepoTest.kt @@ -0,0 +1,422 @@ +package com.orgzly.android.repos + +import android.annotation.SuppressLint +import android.net.Uri +import android.os.Build +import androidx.documentfile.provider.DocumentFile +import com.orgzly.android.BookName +import com.orgzly.android.util.MiscUtils +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.storage.file.FileRepositoryBuilder +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import java.io.File +import java.io.IOException + +@SuppressLint("NewApi") +interface SyncRepoTest { + + fun testGetBooks_singleOrgFile() + fun testGetBooks_singleFileInSubfolder() + fun testGetBooks_allFilesAreIgnored() + fun testGetBooks_specificFileInSubfolderIsIgnored() + fun testGetBooks_specificFileIsUnignored() + fun testGetBooks_ignoredExtensions() + fun testStoreBook_expectedUri() + fun testStoreBook_producesSameUriAsRetrieveBook() + fun testStoreBook_producesSameUriAsGetBooks() + fun testStoreBook_inSubfolder() + fun testRenameBook_expectedUri() + fun testRenameBook_repoFileAlreadyExists() + fun testRenameBook_fromRootToSubfolder() + fun testRenameBook_fromSubfolderToRoot() + fun testRenameBook_newSubfolderSameLeafName() + fun testRenameBook_newSubfolderAndLeafName() + fun testRenameBook_sameSubfolderNewLeafName() + + companion object { + + const val repoDirName = "orgzly-android-test" + private var treeDocumentFileExtraSegment = if (Build.VERSION.SDK_INT < 30) { + "/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2F$repoDirName%2F" + } else { + "/document/primary%3A$repoDirName%2F" + } + + fun testGetBooks_singleOrgFile(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val fileContent = "\n\n...\n\n" + val fileName = "Book one.org" + val expectedRookUri = writeFileToRepo(fileContent, syncRepo, repoManipulationPoint, fileName) + + // When + val books = syncRepo.books + val retrieveBookDestinationFile = kotlin.io.path.createTempFile().toFile() + syncRepo.retrieveBook(fileName, retrieveBookDestinationFile) + + // Then + assertEquals(1, books.size) + assertEquals(expectedRookUri, books[0].uri.toString()) + assertEquals(fileContent, retrieveBookDestinationFile.readText()) + assertEquals(fileName, BookName.getRepoRelativePath(syncRepo.uri, books[0].uri)) + } + + fun testGetBooks_singleFileInSubfolder(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val repoFilePath = "Folder/Book one.org" + val fileContent = "\n\n...\n\n" + val expectedRookUri = writeFileToRepo(fileContent, syncRepo, repoManipulationPoint, "Book one.org", "Folder") + + // When + val books = syncRepo.books + val retrieveBookDestinationFile = kotlin.io.path.createTempFile().toFile() + syncRepo.retrieveBook(repoFilePath, retrieveBookDestinationFile) + + // Then + assertEquals(1, books.size) + assertEquals(expectedRookUri, books[0].uri.toString()) + assertEquals(repoFilePath, BookName.getRepoRelativePath(syncRepo.uri, books[0].uri)) + assertEquals(fileContent, retrieveBookDestinationFile.readText()) + } + + fun testGetBooks_allFilesAreIgnored(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val ignoreFileContent = "*\n" + writeFileToRepo("...", syncRepo, repoManipulationPoint, "book one.org", "folder") + writeFileToRepo(ignoreFileContent, syncRepo, repoManipulationPoint, RepoIgnoreNode.IGNORE_FILE) + // When + val books = syncRepo.books + // Then + assertEquals(0, books.size) + } + + fun testGetBooks_specificFileInSubfolderIsIgnored(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val ignoreFileContent = "folder/book one.org\n" + writeFileToRepo("...", syncRepo, repoManipulationPoint, "book one.org", "folder") + writeFileToRepo(ignoreFileContent, syncRepo, repoManipulationPoint, RepoIgnoreNode.IGNORE_FILE) + // When + val books = syncRepo.books + // Then + assertEquals(0, books.size) + } + fun testGetBooks_specificFileIsUnignored(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val folderName = "My Folder" + val fileName = "My file.org" + val ignoreFileContent = "folder/**\n!$folderName/$fileName\n" + writeFileToRepo("...", syncRepo, repoManipulationPoint, fileName, folderName) + writeFileToRepo(ignoreFileContent, syncRepo, repoManipulationPoint, RepoIgnoreNode.IGNORE_FILE) + // When + val books = syncRepo.books + // Then + assertEquals(1, books.size) + } + + fun testGetBooks_ignoredExtensions(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val testBookContent = "\n\n...\n\n" + for (fileName in arrayOf("file one.txt", "file two.o", "file three.org")) { + writeFileToRepo(testBookContent, syncRepo, repoManipulationPoint, fileName) + } + // When + val books = syncRepo.books + // Then + assertEquals(1, books.size.toLong()) + assertEquals("file three", BookName.fromRepoRelativePath(BookName.getRepoRelativePath(syncRepo.uri, books[0].uri)).name) + } + + fun testStoreBook_expectedUri(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + MiscUtils.writeStringToFile("...", tmpFile) + // When + val vrook = syncRepo.storeBook(tmpFile, "Book one.org") + tmpFile.delete() + // Then + val expectedRookUri = when (syncRepo) { + is GitRepo -> "/Book one.org" + is DocumentRepo -> syncRepo.uri.toString() + treeDocumentFileExtraSegment + "Book%20one.org" + else -> syncRepo.uri.toString() + "/Book%20one.org" + } + assertEquals(expectedRookUri, vrook.uri.toString()) + } + + fun testStoreBook_producesSameUriAsRetrieveBook(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + val repositoryPath = "a folder/a book.org" + MiscUtils.writeStringToFile("...", tmpFile) + // When + val storedRook = syncRepo.storeBook(tmpFile, repositoryPath) + val retrievedBook = syncRepo.retrieveBook(repositoryPath, tmpFile) + tmpFile.delete() + // Then + assertEquals(retrievedBook.uri, storedRook.uri) + } + + fun testStoreBook_producesSameUriAsGetBooks(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + val folderName = "A folder" + val fileName = "A book.org" + writeFileToRepo("...", syncRepo, repoManipulationPoint, fileName, folderName) + // When + val gottenBook = syncRepo.books[0] + MiscUtils.writeStringToFile("......", tmpFile) // N.B. Different content to ensure the repo file is actually changed + val storedRook = syncRepo.storeBook(tmpFile, "$folderName/$fileName") + tmpFile.delete() + // Then + assertEquals(gottenBook.uri, storedRook.uri) + } + + fun testStoreBook_inSubfolder(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + val repositoryPath = "A folder/A book.org" + val testBookContent = "\n\n...\n\n" + MiscUtils.writeStringToFile(testBookContent, tmpFile) + // When + syncRepo.storeBook(tmpFile, repositoryPath) + tmpFile.delete() + // Then + when (syncRepo) { + is WebdavRepo -> { + repoManipulationPoint as File + val subFolder = File(repoManipulationPoint, "A folder") + assertTrue(subFolder.exists()) + val bookFile = File(subFolder, "A book.org") + assertTrue(bookFile.exists()) + assertEquals(testBookContent, bookFile.readText()) + } + is GitRepo -> { + repoManipulationPoint as File + val git = Git( + FileRepositoryBuilder() + .addCeilingDirectory(repoManipulationPoint) + .findGitDir(repoManipulationPoint) + .build() + ) + git.pull().call() + val subFolder = File(repoManipulationPoint, "A folder") + assertTrue(subFolder.exists()) + val bookFile = File(subFolder, "A book.org") + assertTrue(bookFile.exists()) + assertEquals(testBookContent, bookFile.readText()) + } + is DocumentRepo -> { + repoManipulationPoint as DocumentFile + val subFolder = repoManipulationPoint.findFile("A folder") + assertTrue(subFolder!!.exists()) + assertTrue(subFolder.isDirectory) + val bookFile = subFolder.findFile("A book.org") + assertTrue(bookFile!!.exists()) + assertEquals(testBookContent, MiscUtils.readStringFromDocumentFile(bookFile)) + } + is DropboxRepo -> { + // Not really much to assert here; we don't really care how Dropbox implements things, + // as long as URLs work as expected. + repoManipulationPoint as DropboxClient + val retrievedFile = kotlin.io.path.createTempFile().toFile() + repoManipulationPoint.download(syncRepo.uri, repositoryPath, retrievedFile) + assertEquals(testBookContent, retrievedFile.readText()) + } + } + } + + fun testRenameBook_expectedUri(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + val oldFileName = "Original book.org" + val newBookName = "Renamed book" + val testBookContent = "\n\n...\n\n" + MiscUtils.writeStringToFile(testBookContent, tmpFile) + // When + val originalVrook = syncRepo.storeBook(tmpFile, oldFileName) + tmpFile.delete() + syncRepo.renameBook(originalVrook.uri, newBookName) + // Then + val renamedVrook = syncRepo.books[0] + val expectedRookUri = when (syncRepo) { + is GitRepo -> "/Renamed book.org" + is DocumentRepo -> syncRepo.uri.toString() + treeDocumentFileExtraSegment + "Renamed%20book.org" + else -> syncRepo.uri.toString() + "/Renamed%20book.org" + } + assertEquals(expectedRookUri, renamedVrook.uri.toString()) + } + + fun testRenameBook_repoFileAlreadyExists(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + for (fileName in arrayOf("Original.org", "Renamed.org")) { + writeFileToRepo("...", syncRepo, repoManipulationPoint, fileName) + } + val retrievedBookFile = kotlin.io.path.createTempFile().toFile() + // When + val originalRook = syncRepo.retrieveBook("Original.org", retrievedBookFile) + try { + syncRepo.renameBook(originalRook.uri, "Renamed") + } catch (e: IOException) { + // Then + assertTrue(e.message!!.contains("Renamed.org already exists")) + throw e + } finally { + retrievedBookFile.delete() + } + } + + fun testRenameBook_fromRootToSubfolder(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + MiscUtils.writeStringToFile("...", tmpFile) + // When + val originalRook = syncRepo.storeBook(tmpFile, "Original book.org") + tmpFile.delete() + val renamedRook = syncRepo.renameBook(originalRook.uri, "A folder/Renamed book") + // Then + val expectedRookUri = when (syncRepo) { + is GitRepo -> "/A folder/Renamed book.org" + is DocumentRepo -> syncRepo.uri.toString() + treeDocumentFileExtraSegment + "A%20folder%2FRenamed%20book.org" + else -> syncRepo.uri.toString() + "/A%20folder/Renamed%20book.org" + } + assertEquals(expectedRookUri, renamedRook.uri.toString()) + } + + fun testRenameBook_fromSubfolderToRoot(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + MiscUtils.writeStringToFile("...", tmpFile) + // When + val originalRook = syncRepo.storeBook(tmpFile, "A folder/Original book.org") + tmpFile.delete() + val renamedRook = syncRepo.renameBook(originalRook.uri, "Renamed book") + // Then + val expectedRookUri = when (syncRepo) { + is GitRepo -> "/Renamed book.org" + is DocumentRepo -> syncRepo.uri.toString() + treeDocumentFileExtraSegment + "Renamed%20book.org" + else -> syncRepo.uri.toString() + "/Renamed%20book.org" + } + assertEquals(expectedRookUri, renamedRook.uri.toString()) + } + + fun testRenameBook_newSubfolderSameLeafName(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + MiscUtils.writeStringToFile("...", tmpFile) + // When + val originalRook = syncRepo.storeBook(tmpFile, "Old folder/Original book.org") + tmpFile.delete() + val renamedRook = syncRepo.renameBook(originalRook.uri, "New folder/Original book") + // Then + val expectedRookUri = when (syncRepo) { + is GitRepo -> "/New folder/Original book.org" + is DocumentRepo -> syncRepo.uri.toString() + treeDocumentFileExtraSegment + "New%20folder%2FOriginal%20book.org" + else -> syncRepo.uri.toString() + "/New%20folder/Original%20book.org" + } + assertEquals(expectedRookUri, renamedRook.uri.toString()) + } + + fun testRenameBook_newSubfolderAndLeafName(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + MiscUtils.writeStringToFile("...", tmpFile) + // When + val originalRook = syncRepo.storeBook(tmpFile, "old folder/Original book.org") + tmpFile.delete() + val renamedRook = syncRepo.renameBook(originalRook.uri, "new folder/New book") + // Then + val expectedRookUri = when (syncRepo) { + is GitRepo -> "/new folder/New book.org" + is DocumentRepo -> syncRepo.uri.toString() + treeDocumentFileExtraSegment + "new%20folder%2FNew%20book.org" + else -> syncRepo.uri.toString() + "/new%20folder/New%20book.org" + } + assertEquals(expectedRookUri, renamedRook.uri.toString()) + } + + fun testRenameBook_sameSubfolderNewLeafName(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + MiscUtils.writeStringToFile("...", tmpFile) + // When + val originalRook = syncRepo.storeBook(tmpFile, "old folder/Original book.org") + tmpFile.delete() + val renamedRook = syncRepo.renameBook(originalRook.uri, "old folder/New book") + // Then + val expectedRookUri = when (syncRepo) { + is GitRepo -> "/old folder/New book.org" + is DocumentRepo -> syncRepo.uri.toString() + treeDocumentFileExtraSegment + "old%20folder%2FNew%20book.org" + else -> syncRepo.uri.toString() + "/old%20folder/New%20book.org" + } + assertEquals(expectedRookUri, renamedRook.uri.toString()) + } + + private fun writeFileToRepo( + content: String, + repo: SyncRepo, + repoManipulationPoint: Any, + fileName: String, + folderName: String? = null + ): String { + var expectedRookUri = repo.uri.toString() + "/" + Uri.encode(fileName) + when (repo) { + is WebdavRepo -> { + var targetDir = repoManipulationPoint as File + if (folderName != null) { + targetDir = File(targetDir.absolutePath + "/$folderName") + targetDir.mkdir() + expectedRookUri = repo.uri.toString() + "/" + Uri.encode("$folderName/$fileName", "/") + } + val remoteBookFile = File(targetDir.absolutePath + "/$fileName") + MiscUtils.writeStringToFile(content, remoteBookFile) + } + is GitRepo -> { + expectedRookUri = "/$fileName" + var targetDir = repoManipulationPoint as File + if (folderName != null) { + expectedRookUri = "/$folderName/$fileName" + targetDir = File(targetDir.absolutePath + "/$folderName") + targetDir.mkdir() + } + MiscUtils.writeStringToFile( + content, + File(targetDir.absolutePath + "/$fileName") + ) + updateGitRepo(repoManipulationPoint) + } + is DocumentRepo -> { + expectedRookUri = repo.uri.toString() + treeDocumentFileExtraSegment + Uri.encode(fileName) + var targetDir = repoManipulationPoint as DocumentFile + if (folderName != null) { + targetDir = targetDir.createDirectory(folderName)!! + expectedRookUri = repo.uri.toString() + treeDocumentFileExtraSegment + Uri.encode("$folderName/$fileName") + } + MiscUtils.writeStringToDocumentFile(content, fileName, targetDir.uri) + } + is DropboxRepo -> { + repoManipulationPoint as DropboxClient + val tmpFile = kotlin.io.path.createTempFile().toFile() + MiscUtils.writeStringToFile(content, tmpFile) + var targetPath = fileName + if (folderName != null) { + targetPath = "$folderName/$fileName" + expectedRookUri = repo.uri.toString() + "/" + Uri.encode("$folderName/$fileName", "/") + } + repoManipulationPoint.upload(tmpFile, repo.uri, targetPath) + tmpFile.delete() + } + } + return expectedRookUri + } + + private fun updateGitRepo(workdir: File) { + val git = Git( + FileRepositoryBuilder() + .addCeilingDirectory(workdir) + .findGitDir(workdir) + .build() + ) + git.add().addFilepattern(".").call() + git.commit().setMessage("").call() + git.push().call() + } + } +} \ No newline at end of file From fa0ca58623a1f6b4348fe9f9dde2933bfdb26ba6 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Sun, 25 Aug 2024 21:40:10 +0200 Subject: [PATCH 15/18] Skip transforming problematic JAR with Jetify --- gradle.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 877be9ba9..bdcc4dff5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,4 +18,5 @@ android.databinding.incremental = true kotlin.code.style = official org.gradle.unsafe.configuration-cache=true -# org.gradle.warning.mode=all \ No newline at end of file +# org.gradle.warning.mode=all +android.jetifier.ignorelist = bcprov-jdk18on-1.78.1.jar From f91b4074954dcf70844e4d00060aac682c10a3d5 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Sun, 25 Aug 2024 21:53:19 +0200 Subject: [PATCH 16/18] Don't instantiate external access action handlers upon app launch Since external access typically happens either infrequently or in an automated fashion, any small delay this instantiation creates should be tolerable. The reason for this change is that it allows us to run local JVM tests. Before this change, the app could not run without a real Android device and its Context. --- .../com/orgzly/android/external/ExternalAccessReceiver.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt b/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt index 97b8ec116..300f3d41b 100644 --- a/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt +++ b/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt @@ -8,15 +8,14 @@ import com.orgzly.android.external.actionhandlers.* import com.orgzly.android.external.types.Response class ExternalAccessReceiver : BroadcastReceiver() { - val actionHandlers = listOf( + override fun onReceive(context: Context?, intent: Intent?) { + val actionHandlers = listOf( GetOrgInfo(), RunSearch(), EditNotes(), EditSavedSearches(), ManageWidgets() - ) - - override fun onReceive(context: Context?, intent: Intent?) { + ) val response = actionHandlers.asSequence() .mapNotNull { it.handle(intent!!, context!!) } .firstOrNull() From 8af6c2472954d074f768d07cf6ee9bb9396e1a39 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Mon, 26 Aug 2024 08:12:44 +0200 Subject: [PATCH 17/18] A few more sleeps in flaky Espresso tests --- .../orgzly/android/espresso/NoteEventsTest.kt | 16 +++++++++++++--- .../android/espresso/QueryFragmentTest.java | 2 ++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/NoteEventsTest.kt b/app/src/androidTest/java/com/orgzly/android/espresso/NoteEventsTest.kt index 4eaa46e14..d037f594d 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/NoteEventsTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/espresso/NoteEventsTest.kt @@ -1,16 +1,25 @@ package com.orgzly.android.espresso -import android.os.SystemClock import android.icu.util.Calendar +import android.os.SystemClock import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.longClick import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText import com.orgzly.R import com.orgzly.android.OrgzlyTest -import com.orgzly.android.espresso.util.EspressoUtils.* +import com.orgzly.android.espresso.util.EspressoUtils.onBook +import com.orgzly.android.espresso.util.EspressoUtils.onItemInAgenda +import com.orgzly.android.espresso.util.EspressoUtils.onNoteInBook +import com.orgzly.android.espresso.util.EspressoUtils.onNoteInSearch +import com.orgzly.android.espresso.util.EspressoUtils.onNotesInAgenda +import com.orgzly.android.espresso.util.EspressoUtils.onNotesInSearch +import com.orgzly.android.espresso.util.EspressoUtils.recyclerViewItemCount +import com.orgzly.android.espresso.util.EspressoUtils.searchForTextCloseKeyboard import com.orgzly.android.ui.main.MainActivity import com.orgzly.org.datetime.OrgDateTime import org.hamcrest.Matchers.not @@ -117,6 +126,7 @@ class NoteEventsTest : OrgzlyTest() { scenario = ActivityScenario.launch(MainActivity::class.java) searchForTextCloseKeyboard("ad.1") + SystemClock.sleep(500) onNotesInAgenda().check(matches(recyclerViewItemCount(2))) } diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/QueryFragmentTest.java b/app/src/androidTest/java/com/orgzly/android/espresso/QueryFragmentTest.java index 0afbd5ef3..ec429e787 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/QueryFragmentTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/QueryFragmentTest.java @@ -385,6 +385,7 @@ public void testInheritedAndOwnTag() { scenario = ActivityScenario.launch(MainActivity.class); onView(allOf(withText("notebook-1"), isDisplayed())).perform(click()); + SystemClock.sleep(200); searchForTextCloseKeyboard("t.tag1 t.tag2"); onView(withId(R.id.fragment_query_search_view_flipper)).check(matches(isDisplayed())); onNotesInSearch().check(matches(recyclerViewItemCount(3))); @@ -725,6 +726,7 @@ public void testSearchWithState() { scenario = ActivityScenario.launch(MainActivity.class); onView(allOf(withText("notebook"), isDisplayed())).perform(click()); + SystemClock.sleep(200); searchForTextCloseKeyboard(".it.none"); onView(withId(R.id.fragment_query_search_view_flipper)).check(matches(isDisplayed())); onNotesInSearch().check(matches(recyclerViewItemCount(3))); From 4b0560e0b7a9980bb170473c54a49c6db1411a62 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Mon, 26 Aug 2024 08:55:50 +0200 Subject: [PATCH 18/18] Tidy up dependencies - Gather all test dependencies in one place - Clean up some unused stuff - Consistent syntax --- app/build.gradle | 26 ++++++++++---------------- build.gradle | 4 ++++ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 394d560ae..b66898e91 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -116,7 +116,7 @@ android { dependencies { implementation orgJavaLocation() - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.kotlin_coroutines") + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.kotlin_coroutines" implementation "org.jetbrains:annotations:$versions.jetbrains_annotations" @@ -138,7 +138,6 @@ dependencies { // Room implementation "androidx.room:room-runtime:$versions.android_room" - testImplementation "androidx.room:room-testing:$versions.android_room" kapt "androidx.room:room-compiler:$versions.android_room" implementation("androidx.room:room-ktx:$versions.android_room") @@ -147,34 +146,31 @@ dependencies { implementation("androidx.lifecycle:lifecycle-livedata-ktx:$versions.android_lifecycle") implementation("androidx.lifecycle:lifecycle-runtime-ktx:$versions.android_lifecycle") - androidTestImplementation "androidx.annotation:annotation:$versions.android_annotation" - implementation "androidx.work:work-runtime-ktx:$versions.android_workmanager" + // Local JVM tests ("unit tests") + testImplementation(project(":shared-test")) testImplementation "androidx.test.ext:junit:$versions.android_test_ext_junit" - testImplementation 'org.robolectric:robolectric:4.13' - testImplementation "io.github.atetzner:webdav-embedded-server:0.2.1" + testImplementation "org.robolectric:robolectric:$versions.robolectric" + testImplementation "io.github.atetzner:webdav-embedded-server:$versions.webdav_embedded_server" - // AndroidX Test + // Android instrumented tests + androidTestImplementation(project(":shared-test")) androidTestImplementation "androidx.test.espresso:espresso-core:$versions.android_test_espresso" androidTestImplementation "androidx.test.espresso:espresso-contrib:$versions.android_test_espresso" androidTestImplementation "androidx.test.espresso:espresso-intents:$versions.android_test_espresso" androidTestImplementation "androidx.test:runner:$versions.android_test" androidTestImplementation "androidx.test:rules:$versions.android_test" androidTestImplementation "androidx.test.ext:junit:$versions.android_test_ext_junit" + androidTestImplementation "androidx.test.uiautomator:uiautomator:$versions.android_test_uiautomator" + androidTestImplementation "de.sven-jacobs:loremipsum:$versions.loremipsum" + androidTestImplementation "androidx.annotation:annotation:$versions.android_annotation" /* For running tests on lower API versions (e.g. 18) to avoid: * Didn't find class "androidx.test.core.app.InstrumentationActivityInvoker$BootstrapActivity" */ implementation "androidx.test:core:$versions.android_test" - // For ANDROIDX_TEST_ORCHESTRATOR - // androidTestUtil "androidx.test:orchestrator:$android_test_version" - - androidTestImplementation "androidx.test.uiautomator:uiautomator:$versions.android_test_uiautomator" - - androidTestImplementation "de.sven-jacobs:loremipsum:$versions.loremipsum" - // Dagger implementation "com.google.dagger:dagger:$versions.dagger" kapt "com.google.dagger:dagger-compiler:$versions.dagger" @@ -213,8 +209,6 @@ dependencies { implementation("androidx.biometric:biometric-ktx:$versions.biometric_ktx") { because 'Protect SSH key with biometric prompt' } - testImplementation(project(":shared-test")) - androidTestImplementation(project(":shared-test")) } repositories { diff --git a/build.gradle b/build.gradle index 3926e82c0..4a15a34b4 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,10 @@ buildscript { versions.biometric_ktx = '1.2.0-alpha04' + versions.robolectric = '4.13' + + versions.webdav_embedded_server = '0.2.1' + ext.versions = versions repositories {