From cb34c4349bb35b6e65188a95f95b0d61bb2f0c6b Mon Sep 17 00:00:00 2001 From: Helle <54167962+Helle-Daryd@users.noreply.github.com> Date: Tue, 23 Jul 2024 18:06:33 +0200 Subject: [PATCH 01/10] Fixed the image loading We are now requesting the correct permission on for Android sdk level 33 and above. --- app/src/main/AndroidManifest.xml | 3 ++- app/src/main/java/com/orgzly/android/util/AppPermissions.kt | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4242e8453..1f46ad192 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,7 +12,8 @@ - + + diff --git a/app/src/main/java/com/orgzly/android/util/AppPermissions.kt b/app/src/main/java/com/orgzly/android/util/AppPermissions.kt index 426fa497e..0e7d05da5 100644 --- a/app/src/main/java/com/orgzly/android/util/AppPermissions.kt +++ b/app/src/main/java/com/orgzly/android/util/AppPermissions.kt @@ -70,7 +70,10 @@ object AppPermissions { Usage.BOOK_EXPORT -> Manifest.permission.WRITE_EXTERNAL_STORAGE Usage.SYNC_START -> Manifest.permission.WRITE_EXTERNAL_STORAGE Usage.SAVED_SEARCHES_EXPORT_IMPORT -> Manifest.permission.WRITE_EXTERNAL_STORAGE - Usage.EXTERNAL_FILES_ACCESS -> Manifest.permission.READ_EXTERNAL_STORAGE + Usage.EXTERNAL_FILES_ACCESS -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + Manifest.permission.READ_MEDIA_IMAGES + else + Manifest.permission.READ_EXTERNAL_STORAGE Usage.POST_NOTIFICATIONS -> Manifest.permission.POST_NOTIFICATIONS } } From b711871e5852cefbcef831e15cef989ae6f5fc4f Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Wed, 24 Jul 2024 01:41:52 +0200 Subject: [PATCH 02/10] Grant the READ_MEDIA_IMAGES permission during ExternalLinksTest --- .../java/com/orgzly/android/espresso/ExternalLinksTest.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt b/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt index a356f22a8..68faff3ca 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt @@ -6,6 +6,7 @@ import androidx.test.core.app.ActivityScenario import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.rule.GrantPermissionRule import com.orgzly.R import com.orgzly.android.App import com.orgzly.android.OrgzlyTest @@ -15,6 +16,7 @@ import com.orgzly.android.espresso.util.EspressoUtils.onNoteInBook import com.orgzly.android.espresso.util.EspressoUtils.onSnackbar import com.orgzly.android.ui.main.MainActivity import org.hamcrest.Matchers.startsWith +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -25,6 +27,11 @@ class ExternalLinksTest(private val param: Parameter) : OrgzlyTest() { data class Parameter(val link: String, val check: () -> Any) + @get:Rule + val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( + android.Manifest.permission.READ_MEDIA_IMAGES + ) + companion object { @JvmStatic @Parameterized.Parameters(name = "{index}: {0}") From 1323925346f0e97bdbbf3735d1fdf143b81a55b3 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Wed, 24 Jul 2024 14:07:30 +0200 Subject: [PATCH 03/10] Grant permission when running the relevant API version --- .../com/orgzly/android/espresso/ExternalLinksTest.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt b/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt index 68faff3ca..1e0218971 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt @@ -1,5 +1,6 @@ package com.orgzly.android.espresso +import android.os.Build import android.os.Environment import android.os.SystemClock import androidx.test.core.app.ActivityScenario @@ -28,9 +29,11 @@ class ExternalLinksTest(private val param: Parameter) : OrgzlyTest() { data class Parameter(val link: String, val check: () -> Any) @get:Rule - val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( - android.Manifest.permission.READ_MEDIA_IMAGES - ) + val grantPermissionRule: GrantPermissionRule = if (Build.VERSION.SDK_INT >= 33) { + GrantPermissionRule.grant(android.Manifest.permission.READ_MEDIA_IMAGES) + } else { + GrantPermissionRule.grant() + } companion object { @JvmStatic From f9e031c34586ed1c2ea485fbe3f771e0a004f2ea Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Sat, 27 Jul 2024 22:50:11 +0200 Subject: [PATCH 04/10] Release 1.8.25-beta.1 --- app/build.gradle | 4 ++-- app/src/main/res/layout/dialog_whats_new.xml | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 543ad2dcd..8d3c04500 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 33 - versionCode 212 - versionName "1.8.24" + versionCode 213 + versionName "1.8.25-beta.1" applicationId = "com.orgzlyrevived" resValue "string", "application_id", "com.orgzlyrevived" diff --git a/app/src/main/res/layout/dialog_whats_new.xml b/app/src/main/res/layout/dialog_whats_new.xml index 28a7422d9..f36621931 100644 --- a/app/src/main/res/layout/dialog_whats_new.xml +++ b/app/src/main/res/layout/dialog_whats_new.xml @@ -20,6 +20,19 @@ android:layout_height="wrap_content" /> + + + + + + Date: Sun, 9 Jun 2024 00:53:00 +0200 Subject: [PATCH 05/10] Add class RepoIgnoreNode which compares file names against .orgzlyignore rules The class extends the JGit class IgnoreNode, which can do most of the work for us. The SyncRepo interface needed a new method to allow reading an arbitrary file from the repository, since the .orgzlyignore file is not an ORG file. --- .../android/git/GitFileSynchronizer.java | 6 ++ .../com/orgzly/android/repos/ContentRepo.java | 7 +++ .../orgzly/android/repos/DatabaseRepo.java | 6 ++ .../orgzly/android/repos/DirectoryRepo.java | 7 +++ .../orgzly/android/repos/DropboxClient.java | 25 ++++++++ .../com/orgzly/android/repos/DropboxRepo.java | 6 ++ .../com/orgzly/android/repos/GitRepo.java | 7 +++ .../com/orgzly/android/repos/MockRepo.java | 6 ++ .../orgzly/android/repos/RepoIgnoreNode.kt | 61 +++++++++++++++++++ .../com/orgzly/android/repos/SyncRepo.java | 9 +++ .../com/orgzly/android/repos/WebdavRepo.kt | 8 +++ app/src/main/res/values/strings.xml | 2 +- 12 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/orgzly/android/repos/RepoIgnoreNode.kt 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 e976c95f2..cfa0fa4fe 100644 --- a/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java +++ b/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java @@ -29,8 +29,10 @@ import org.eclipse.jgit.treewalk.TreeWalk; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.List; @@ -61,6 +63,10 @@ public void retrieveLatestVersionOfFile( MiscUtils.copyFile(repoDirectoryFile(repositoryPath), destination); } + public InputStream openRepoFileInputStream(String repositoryPath) throws FileNotFoundException { + return new FileInputStream(repoDirectoryFile(repositoryPath)); + } + private void fetch() throws IOException { try { if (BuildConfig.LOG_DEBUG) { 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 0b1148ebd..50e94eaa3 100644 --- a/app/src/main/java/com/orgzly/android/repos/ContentRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/ContentRepo.java @@ -126,6 +126,13 @@ public VersionedRook retrieveBook(String fileName, File destinationFile) throws return new VersionedRook(repoId, RepoType.DOCUMENT, repoUri, sourceFile.getUri(), rev, mtime); } + @Override + public InputStream openRepoFileInputStream(String fileName) throws IOException { + DocumentFile sourceFile = repoDocumentFile.findFile(fileName); + if (sourceFile == null) throw new FileNotFoundException(); + return context.getContentResolver().openInputStream(sourceFile.getUri()); + } + @Override public VersionedRook storeBook(File file, String fileName) throws IOException { if (!file.exists()) { 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 9a2c5554f..7d371a12e 100644 --- a/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java @@ -8,6 +8,7 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.util.List; /** @@ -52,6 +53,11 @@ public VersionedRook retrieveBook(String fileName, File file) { return dbRepo.retrieveBook(repoId, repoUri, uri, file); } + @Override + public InputStream openRepoFileInputStream(String fileName) throws IOException { + throw new UnsupportedOperationException("Not implemented"); + } + @Override public VersionedRook storeBook(File file, String fileName) throws IOException { String content = MiscUtils.readStringFromFile(file); 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 bac17240c..c623b9fd6 100644 --- a/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java @@ -12,8 +12,10 @@ import org.jetbrains.annotations.NotNull; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -130,6 +132,11 @@ public VersionedRook retrieveBook(String fileName, File destinationFile) throws return new VersionedRook(repoId, RepoType.DIRECTORY, repoUri, uri, rev, mtime); } + @Override + public InputStream openRepoFileInputStream(String fileName) throws IOException { + return new FileInputStream(repoUri.buildUpon().appendPath(fileName).build().getPath()); + } + @Override public VersionedRook storeBook(File file, String fileName) throws IOException { if (!file.exists()) { 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 e0fdbe082..12482fadf 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxClient.java +++ b/app/src/main/java/com/orgzly/android/repos/DropboxClient.java @@ -4,6 +4,7 @@ import android.content.Context; import android.net.Uri; +import com.dropbox.core.DbxDownloader; import com.dropbox.core.DbxException; import com.dropbox.core.DbxRequestConfig; import com.dropbox.core.android.Auth; @@ -25,6 +26,7 @@ import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -233,6 +235,29 @@ public VersionedRook download(Uri repoUri, String fileName, File localFile) thro } } + public InputStream streamFile(Uri repoUri, String fileName) throws IOException { + linkedOrThrow(); + + Uri uri = repoUri.buildUpon().appendPath(fileName).build(); + FileMetadata metadata; + String rev; + DbxDownloader downloader; + + try { + Metadata pathMetadata = dbxClient.files().getMetadata(uri.getPath()); + metadata = (FileMetadata) pathMetadata; + rev = metadata.getRev(); + downloader = dbxClient.files().download(metadata.getPathLower(), rev); + } catch (DbxException e) { + if (e instanceof GetMetadataErrorException) { + if (((GetMetadataErrorException) e).errorValue.getPathValue() == LookupError.NOT_FOUND) { + throw new FileNotFoundException(); + } + } + throw new RuntimeException(e); + } + return downloader.getInputStream(); + } /** Upload file to Dropbox. */ public VersionedRook upload(File file, Uri repoUri, String fileName) throws IOException { 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 484da45a6..402ca1594 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java @@ -8,6 +8,7 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.util.List; public class DropboxRepo implements SyncRepo { @@ -46,6 +47,11 @@ public VersionedRook retrieveBook(String fileName, File file) throws IOException return client.download(repoUri, fileName, file); } + @Override + public InputStream openRepoFileInputStream(String fileName) throws IOException { + return client.streamFile(repoUri, fileName); + } + @Override public VersionedRook storeBook(File file, String fileName) throws IOException { return client.upload(file, repoUri, fileName); 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 3d60b5cf6..2e1f31225 100644 --- a/app/src/main/java/com/orgzly/android/repos/GitRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/GitRepo.java @@ -38,6 +38,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -221,6 +222,12 @@ public VersionedRook retrieveBook(String fileName, File destination) throws IOEx return currentVersionedRook(sourceUri); } + @Override + public InputStream openRepoFileInputStream(String fileName) throws IOException { + Uri sourceUri = Uri.parse(fileName); + return synchronizer.openRepoFileInputStream(sourceUri.getPath()); + } + private VersionedRook currentVersionedRook(Uri uri) { RevCommit commit = null; uri = Uri.parse(Uri.decode(uri.toString())); 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 a6fca9e8d..63c6f93e5 100644 --- a/app/src/main/java/com/orgzly/android/repos/MockRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/MockRepo.java @@ -8,6 +8,7 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.util.List; /** @@ -56,6 +57,11 @@ public VersionedRook retrieveBook(String fileName, File file) throws IOException return databaseRepo.retrieveBook(fileName, file); } + @Override + public InputStream openRepoFileInputStream(String fileName) throws IOException { + throw new UnsupportedOperationException("Not implemented"); + } + @Override public VersionedRook storeBook(File file, String fileName) throws IOException { SystemClock.sleep(SLEEP_FOR_STORE_BOOK); diff --git a/app/src/main/java/com/orgzly/android/repos/RepoIgnoreNode.kt b/app/src/main/java/com/orgzly/android/repos/RepoIgnoreNode.kt new file mode 100644 index 000000000..d8fe16411 --- /dev/null +++ b/app/src/main/java/com/orgzly/android/repos/RepoIgnoreNode.kt @@ -0,0 +1,61 @@ +package com.orgzly.android.repos + +import android.os.Build +import androidx.annotation.RequiresApi +import com.orgzly.R +import com.orgzly.android.App +import org.eclipse.jgit.ignore.IgnoreNode +import java.io.FileNotFoundException +import java.io.IOException +import kotlin.io.path.Path + +class RepoIgnoreNode(repo: SyncRepo) : IgnoreNode() { + + init { + try { + val inputStream = repo.openRepoFileInputStream(IGNORE_FILE) + inputStream.use { + parse(it) + } + inputStream.close() + } catch (ignored: FileNotFoundException) {} + } + + @RequiresApi(Build.VERSION_CODES.O) + fun isPathIgnored(pathString: String, isDirectory: Boolean): Boolean { + if (rules.isEmpty()) { + return false + } + val path = Path(pathString) + return when (isIgnored(pathString, isDirectory)) { + MatchResult.IGNORED -> + true + MatchResult.NOT_IGNORED -> + false + MatchResult.CHECK_PARENT -> + if (path.parent != null) { + // Recursive call + isPathIgnored(path.parent.toString(), true) + } else { + false + } + else -> false + } + } + + @RequiresApi(Build.VERSION_CODES.O) + fun ensureFileNameIsNotIgnored(filePath: String) { + if (isPathIgnored(filePath, false)) { + throw IOException( + App.getAppContext().getString( + R.string.error_file_matches_repo_ignore_rule, + IGNORE_FILE, + ) + ) + } + } + + companion object { + const val IGNORE_FILE = ".orgzlyignore" + } +} 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 6b2a7301b..ed22ef2d1 100644 --- a/app/src/main/java/com/orgzly/android/repos/SyncRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/SyncRepo.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.util.List; /** @@ -32,6 +33,14 @@ public interface SyncRepo { */ VersionedRook retrieveBook(String fileName, 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 + * @throws IOException + */ + InputStream openRepoFileInputStream(String fileName) 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 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 e8bcd01e5..bc098a768 100644 --- a/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt +++ b/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt @@ -15,6 +15,7 @@ import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine import okhttp3.OkHttpClient import okio.Buffer import java.io.File +import java.io.FileNotFoundException import java.io.FileOutputStream import java.io.InputStream import java.security.KeyStore @@ -186,6 +187,13 @@ class WebdavRepo( return sardine.list(fileUrl).first().toVersionedRook() } + override fun openRepoFileInputStream(fileName: String): InputStream { + val fileUrl = Uri.withAppendedPath(uri, fileName).toUrl() + if (!sardine.exists(fileUrl)) + throw FileNotFoundException() + return sardine.get(fileUrl) + } + override fun storeBook(file: File?, fileName: String?): VersionedRook { val fileUrl = Uri.withAppendedPath(uri, fileName).toUrl() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 375f73245..e67fec24d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -770,7 +770,7 @@ Error while trying to generate the SSH key pair Message: \n Error: SSH key can only be unlocked from an activity - Repository filename matches a rule in %s + Error: Repository file name matches a rule in %s No change From 7bb7a5421b4a6f83f52a148908a817bcc15faebe Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Sun, 9 Jun 2024 01:07:10 +0200 Subject: [PATCH 06/10] Apply the ignore rules in .orgzlyignore during SyncRepo.getBooks() I also speeded up the Git file tree walk. (We were performing similar checks two times -- first in the TreeFilter.include() method override, and then later in the actual processing of interesting nodes. I have concluded that we can filter out all uninteresting nodes in one place.) --- .../com/orgzly/android/repos/ContentRepo.java | 9 +++++++- .../orgzly/android/repos/DirectoryRepo.java | 16 ++++++++++++-- .../orgzly/android/repos/DropboxClient.java | 9 +++++++- .../com/orgzly/android/repos/DropboxRepo.java | 3 ++- .../com/orgzly/android/repos/GitRepo.java | 22 ++++++++----------- .../com/orgzly/android/repos/WebdavRepo.kt | 18 +++++++++++---- 6 files changed, 55 insertions(+), 22 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 50e94eaa3..ef87d2ac2 100644 --- a/app/src/main/java/com/orgzly/android/repos/ContentRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/ContentRepo.java @@ -21,6 +21,7 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * Using DocumentFile, for devices running Lollipop or later. @@ -69,12 +70,18 @@ public List getBooks() throws IOException { DocumentFile[] files = repoDocumentFile.listFiles(); + RepoIgnoreNode ignores = new RepoIgnoreNode(this); + if (files != null) { // Can't compare TreeDocumentFile // Arrays.sort(files); for (DocumentFile file : files) { - if (BookName.isSupportedFormatFileName(file.getName())) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (ignores.isPathIgnored(Objects.requireNonNull(file.getName()), false)) { + continue; + } + } if (BookName.isSupportedFormatFileName(file.getName())) { if (BuildConfig.LOG_DEBUG) { LogUtils.d(TAG, 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 c623b9fd6..db2b59064 100644 --- a/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java @@ -1,6 +1,7 @@ package com.orgzly.android.repos; import android.net.Uri; +import android.os.Build; import android.util.Log; import com.orgzly.android.BookName; @@ -83,10 +84,21 @@ public Uri getUri() { @Override public List getBooks() { + RepoIgnoreNode ignores = new RepoIgnoreNode(this); + List result = new ArrayList<>(); - File[] files = mDirectory.listFiles((dir, filename) -> - BookName.isSupportedFormatFileName(filename)); + File[] files; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + files = mDirectory.listFiles( + (dir, filename) -> BookName.isSupportedFormatFileName(filename) + && !ignores.isPathIgnored(filename, false) + ); + } else { + files = mDirectory.listFiles( + (dir, filename) -> BookName.isSupportedFormatFileName(filename) + ); + } if (files != null) { Arrays.sort(files); 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 12482fadf..940039415 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxClient.java +++ b/app/src/main/java/com/orgzly/android/repos/DropboxClient.java @@ -3,6 +3,7 @@ import android.app.Activity; import android.content.Context; import android.net.Uri; +import android.os.Build; import com.dropbox.core.DbxDownloader; import com.dropbox.core.DbxException; @@ -129,7 +130,7 @@ private void deleteCredential() { AppPreferences.dropboxSerializedCredential(mContext, null); } - public List getBooks(Uri repoUri) throws IOException { + public List getBooks(Uri repoUri, RepoIgnoreNode ignores) throws IOException { linkedOrThrow(); List list = new ArrayList<>(); @@ -153,6 +154,12 @@ public List getBooks(Uri repoUri) throws IOException { if (metadata instanceof FileMetadata) { FileMetadata file = (FileMetadata) metadata; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (ignores.isPathIgnored(file.getName(), false)) { + continue; + } + } + if (BookName.isSupportedFormatFileName(file.getName())) { Uri uri = repoUri.buildUpon().appendPath(file.getName()).build(); VersionedRook book = new VersionedRook( 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 402ca1594..74dd25a65 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java @@ -39,7 +39,8 @@ public Uri getUri() { @Override public List getBooks() throws IOException { - return client.getBooks(repoUri); + RepoIgnoreNode ignores = new RepoIgnoreNode(this); + return client.getBooks(repoUri, ignores); } @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 2e1f31225..0b18bfa85 100644 --- a/app/src/main/java/com/orgzly/android/repos/GitRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/GitRepo.java @@ -291,14 +291,18 @@ public List getBooks() throws IOException { walk.reset(); walk.setRecursive(true); walk.addTree(synchronizer.currentHead().getTree()); - final IgnoreNode ignores = getIgnores(); + final RepoIgnoreNode ignores = new RepoIgnoreNode(this); walk.setFilter(new TreeFilter() { @Override public boolean include(TreeWalk walker) { - final FileMode mode = walker.getFileMode(0); - final String filePath = walker.getPathString(); + final FileMode mode = walk.getFileMode(); final boolean isDirectory = mode == FileMode.TREE; - return !(ignores.isIgnored(filePath, isDirectory) == IgnoreNode.MatchResult.IGNORED); + final String filePath = walk.getPathString(); + if (ignores.isIgnored(filePath, isDirectory) == IgnoreNode.MatchResult.IGNORED) + return false; + if (isDirectory) + return true; + return BookName.isSupportedFormatFileName(filePath); } @Override @@ -312,15 +316,7 @@ public TreeFilter clone() { } }); while (walk.next()) { - final FileMode mode = walk.getFileMode(0); - final boolean isDirectory = mode == FileMode.TREE; - final String filePath = walk.getPathString(); - if (isDirectory) - continue; - if (BookName.isSupportedFormatFileName(filePath)) - result.add( - currentVersionedRook( - Uri.withAppendedPath(Uri.EMPTY, walk.getPathString()))); + result.add(currentVersionedRook(Uri.withAppendedPath(Uri.EMPTY, walk.getPathString()))); } return result; } 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 bc098a768..da62353a2 100644 --- a/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt +++ b/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt @@ -1,6 +1,7 @@ package com.orgzly.android.repos import android.net.Uri +import android.os.Build import com.burgstaller.okhttp.AuthenticationCacheInterceptor import com.burgstaller.okhttp.CachingAuthenticatorDecorator import com.burgstaller.okhttp.DispatchingAuthenticator @@ -26,7 +27,6 @@ import java.util.concurrent.TimeUnit import javax.net.ssl.SSLContext import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager -import kotlin.time.Duration class WebdavRepo( @@ -163,13 +163,23 @@ class WebdavRepo( sardine.createDirectory(url) } + val ignores = RepoIgnoreNode(this) + return sardine .list(url) .mapNotNull { - if (it.isDirectory || !BookName.isSupportedFormatFileName(it.name)) { - null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (it.isDirectory || !BookName.isSupportedFormatFileName(it.name) || ignores.isPathIgnored(it.name, false)) { + null + } else { + it.toVersionedRook() + } } else { - it.toVersionedRook() + if (it.isDirectory || !BookName.isSupportedFormatFileName(it.name)) { + null + } else { + it.toVersionedRook() + } } } .toMutableList() From 36b24514f4b67c102e5e13764a548c3a597751d0 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Sun, 9 Jun 2024 01:12:57 +0200 Subject: [PATCH 07/10] Always check ignore rules when linking and renaming books --- .../com/orgzly/android/data/DataRepository.kt | 14 ++++++- .../com/orgzly/android/repos/GitRepo.java | 39 ++----------------- .../com/orgzly/android/repos/RepoUtils.java | 9 +++++ .../java/com/orgzly/android/sync/SyncUtils.kt | 2 + 4 files changed, 27 insertions(+), 37 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 5b8067594..a4f08fcbf 100644 --- a/app/src/main/java/com/orgzly/android/data/DataRepository.kt +++ b/app/src/main/java/com/orgzly/android/data/DataRepository.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.content.res.Resources import android.media.MediaScannerConnection import android.net.Uri +import android.os.Build import android.os.Handler import android.text.TextUtils import androidx.lifecycle.LiveData @@ -52,7 +53,6 @@ import com.orgzly.org.parser.OrgParser import com.orgzly.org.parser.OrgParserWriter import com.orgzly.org.utils.StateChangeLogic import java.io.* -import java.lang.IllegalStateException import java.util.* import java.util.concurrent.Callable import javax.inject.Inject @@ -384,6 +384,10 @@ class DataRepository @Inject constructor( bookView.syncedTo?.let { vrook -> val repo = getRepoInstance(vrook.repoId, vrook.repoType, vrook.repoUri.toString()) + /* 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)) + val movedVrook = repo.renameBook(vrook.uri, name) updateBookLinkAndSync(book.id, movedVrook) @@ -505,6 +509,14 @@ class DataRepository @Inject constructor( "Repo ${repo.url} not found" }.id + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // 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) + } + db.bookLink().upsert(bookId, repoId) } 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 0b18bfa85..4bd759889 100644 --- a/app/src/main/java/com/orgzly/android/repos/GitRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/GitRepo.java @@ -7,8 +7,7 @@ import android.util.Log; import com.orgzly.BuildConfig; -import com.orgzly.R; -import com.orgzly.android.App; +import com.orgzly.android.BookFormat; import com.orgzly.android.BookName; import com.orgzly.android.db.entity.Repo; import com.orgzly.android.git.GitFileSynchronizer; @@ -35,7 +34,6 @@ import org.eclipse.jgit.util.FileUtils; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -45,7 +43,6 @@ public class GitRepo implements SyncRepo, TwoWaySyncRepo { private final static String TAG = GitRepo.class.getName(); private final long repoId; - private final Context context = App.getAppContext(); /** * Used as cause when we try to clone into a non-empty directory @@ -189,7 +186,6 @@ public boolean isAutoSyncSupported() { } public VersionedRook storeBook(File file, String fileName) throws IOException { - ensureFileNameIsNotIgnored(fileName); File destination = synchronizer.repoDirectoryFile(fileName); if (destination.exists()) { @@ -241,34 +237,6 @@ private VersionedRook currentVersionedRook(Uri uri) { return new VersionedRook(repoId, RepoType.GIT, getUri(), uri, commit.name(), mtime); } - private IgnoreNode getIgnores() throws IOException { - IgnoreNode ignores = new IgnoreNode(); - File ignoreFile = synchronizer.repoDirectoryFile(context.getString(R.string.repo_ignore_rules_file)); - if (ignoreFile.exists()) { - FileInputStream in = new FileInputStream(ignoreFile); - try { - ignores.parse(in); - } finally { - in.close(); - } - } - return ignores; - } - - /** - * Since subdirectories are currently not supported, we only check the file name against the - * ignore rules. - * @param fileName Name of the file which the user tries to write to - * @throws IOException - */ - private void ensureFileNameIsNotIgnored(String fileName) throws IOException { - IgnoreNode ignores = getIgnores(); - if (ignores.isIgnored(fileName, false) == IgnoreNode.MatchResult.IGNORED) { - throw new IOException(context.getString(R.string.error_file_matches_repo_ignore_rule, - context.getString(R.string.repo_ignore_rules_file))); - } - } - public boolean isUnchanged() throws IOException { // Check if the current head is unchanged. // If so, we can read all the VersionedRooks from the database. @@ -329,10 +297,9 @@ public void delete(Uri uri) throws IOException { if (synchronizer.deleteFileFromRepo(uri)) synchronizer.tryPush(); } - public VersionedRook renameBook(Uri oldUri, String newRookName) throws IOException { + public VersionedRook renameBook(Uri oldUri, String newBookName) throws IOException { String oldFileName = oldUri.toString().replaceFirst("^/", ""); - String newFileName = newRookName + ".org"; - ensureFileNameIsNotIgnored(newFileName); + String newFileName = BookName.fileName(newBookName, 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/RepoUtils.java b/app/src/main/java/com/orgzly/android/repos/RepoUtils.java index 1b1a73569..639e77cc2 100644 --- a/app/src/main/java/com/orgzly/android/repos/RepoUtils.java +++ b/app/src/main/java/com/orgzly/android/repos/RepoUtils.java @@ -1,5 +1,9 @@ package com.orgzly.android.repos; +import android.os.Build; + +import androidx.annotation.RequiresApi; + import java.util.Collection; public class RepoUtils { @@ -26,5 +30,10 @@ public static boolean isAutoSyncSupported(Collection repos) { } return true; } + + @RequiresApi(api = Build.VERSION_CODES.O) + public static void ensureFileNameIsNotIgnored(SyncRepo repo, String fileName) { + new RepoIgnoreNode(repo).ensureFileNameIsNotIgnored(fileName); + } } 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 8d36f1873..714592a11 100644 --- a/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt +++ b/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt @@ -157,6 +157,8 @@ object SyncUtils { repoEntity = dataRepository.getRepos().iterator().next() repoUrl = repoEntity.url fileName = 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) bookAction = BookAction.forNow(BookAction.Type.INFO, namesake.status.msg(repoUrl)) } From 453b085b876ec6a068d8ca8b42502cd7d1945dba Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Sun, 9 Jun 2024 01:14:20 +0200 Subject: [PATCH 08/10] Add/update tests for .orgzlyignore support --- .../java/com/orgzly/android/TestUtils.java | 31 ++++ .../orgzly/android/espresso/SyncingTest.java | 12 +- .../android/repos/DirectoryRepoTest.java | 23 +++ .../orgzly/android/repos/DropboxRepoTest.java | 80 +++++++-- .../com/orgzly/android/repos/GitRepoTest.kt | 152 ++++++++++++++++++ .../android/repos/RepoIgnoreNodeTest.kt | 47 ++++++ .../git/GitPreferencesFromRepoPrefs.java | 16 +- .../orgzly/android/prefs/AppPreferences.java | 4 + .../com/orgzly/android/repos/MockRepo.java | 3 +- 9 files changed, 338 insertions(+), 30 deletions(-) create mode 100644 app/src/androidTest/java/com/orgzly/android/repos/GitRepoTest.kt create mode 100644 app/src/androidTest/java/com/orgzly/android/repos/RepoIgnoreNodeTest.kt diff --git a/app/src/androidTest/java/com/orgzly/android/TestUtils.java b/app/src/androidTest/java/com/orgzly/android/TestUtils.java index 297f3edd9..07eb863a3 100644 --- a/app/src/androidTest/java/com/orgzly/android/TestUtils.java +++ b/app/src/androidTest/java/com/orgzly/android/TestUtils.java @@ -5,11 +5,13 @@ import android.net.Uri; +import com.orgzly.BuildConfig; import com.orgzly.android.data.DataRepository; import com.orgzly.android.data.DbRepoBookRepository; import com.orgzly.android.db.entity.BookAction; import com.orgzly.android.db.entity.BookView; import com.orgzly.android.db.entity.Repo; +import com.orgzly.android.prefs.AppPreferences; import com.orgzly.android.repos.RepoType; import com.orgzly.android.repos.RepoWithProps; import com.orgzly.android.repos.SyncRepo; @@ -18,6 +20,10 @@ import com.orgzly.android.sync.SyncUtils; import com.orgzly.android.util.MiscUtils; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assume; + import java.io.File; import java.io.IOException; import java.util.Map; @@ -149,4 +155,29 @@ public Map sync() { return null; } + + public Map syncOrThrow() throws Exception { + Map nameGroups = SyncUtils.groupAllNotebooksByName(dataRepository); + + for (BookNamesake group : nameGroups.values()) { + BookAction action = SyncUtils.syncNamesake(dataRepository, group); + dataRepository.setBookLastActionAndSyncStatus( + group.getBook().getBook().getId(), action, group.getStatus().toString()); + } + + return nameGroups; + } + + public void dropboxTestPreflight() throws JSONException { + Assume.assumeTrue(BuildConfig.IS_DROPBOX_ENABLED); + Assume.assumeTrue(BuildConfig.DROPBOX_APP_KEY.length() > 0); + Assume.assumeTrue(BuildConfig.DROPBOX_REFRESH_TOKEN.length() > 0); + + JSONObject mockSerializedDbxCredential = new 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(App.getAppContext(), mockSerializedDbxCredential.toString()); + } } 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 71290172e..d82bdf1ec 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/SyncingTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/SyncingTest.java @@ -35,7 +35,6 @@ import androidx.test.core.app.ActivityScenario; -import com.orgzly.BuildConfig; import com.orgzly.R; import com.orgzly.android.OrgzlyTest; import com.orgzly.android.RetryTestRule; @@ -45,9 +44,9 @@ import com.orgzly.android.sync.SyncRunner; import com.orgzly.android.ui.main.MainActivity; +import org.json.JSONException; import org.junit.After; import org.junit.Assert; -import org.junit.Assume; import org.junit.Rule; import org.junit.Test; @@ -659,9 +658,8 @@ public void testEncodingAfterSyncSaving() { } @Test - public void testSettingLinkToRenamedRepo() { - Assume.assumeTrue(BuildConfig.IS_DROPBOX_ENABLED); - + public void testSettingLinkToRenamedRepo() throws JSONException { + testUtils.dropboxTestPreflight(); Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); testUtils.setupRook(repo, "mock://repo-a/booky.org", "Täht", "1abcde", 1400067156000L); scenario = ActivityScenario.launch(MainActivity.class); @@ -715,8 +713,8 @@ public void testSettingLinkToRenamedRepo() { } @Test - public void testRenamingReposRemovesLinksWhatUsedThem() { - Assume.assumeTrue(BuildConfig.IS_DROPBOX_ENABLED); + public void testRenamingReposRemovesLinksWhatUsedThem() throws JSONException { + testUtils.dropboxTestPreflight(); testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); testUtils.setupRepo(RepoType.MOCK, "mock://repo-b"); 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 d2aaf23fd..60133d30c 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java @@ -1,5 +1,6 @@ package com.orgzly.android.repos; +import android.os.Build; import android.os.Environment; import com.orgzly.android.BookName; @@ -20,6 +21,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeTrue; public class DirectoryRepoTest extends OrgzlyTest { private static final String TAG = DirectoryRepoTest.class.getName(); @@ -84,6 +86,27 @@ public void testExtension() throws IOException { assertEquals(repoUriString + "/03.org", books.get(0).getUri().toString()); } + @Test + public void testGetBooksRespectsIgnoreRules() throws IOException { + assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O); + RepoWithProps repoWithProps = new RepoWithProps(new Repo(13, RepoType.DIRECTORY, repoUriString)); + DirectoryRepo repo = new DirectoryRepo(repoWithProps, true); + + // Add .org files + MiscUtils.writeStringToFile("content", new File(dirFile, "file1.org")); + MiscUtils.writeStringToFile("content", new File(dirFile, "file2.org")); + MiscUtils.writeStringToFile("content", new File(dirFile, "file3.org")); + + // Add .orgzlyignore file + MiscUtils.writeStringToFile("*1.org\nfile3*", new File(dirFile, RepoIgnoreNode.IGNORE_FILE)); + + List books = repo.getBooks(); + + assertEquals(1, books.size()); + assertEquals("file2", BookName.getInstance(context, books.get(0)).getName()); + assertEquals(repoUriString + "/file2.org", books.get(0).getUri().toString()); + } + @Test public void testListDownloadsDirectory() throws IOException { File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); 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 90f9bbe4b..38a9ba6e0 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java @@ -1,25 +1,26 @@ package com.orgzly.android.repos; -import com.orgzly.BuildConfig; +import com.orgzly.android.App; import com.orgzly.android.BookName; import com.orgzly.android.OrgzlyTest; import com.orgzly.android.db.entity.BookView; -import com.orgzly.android.prefs.AppPreferences; import com.orgzly.android.util.MiscUtils; -import org.json.JSONObject; -import org.junit.Assume; 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.List; import java.util.UUID; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; -import android.os.SystemClock; +import android.net.Uri; public class DropboxRepoTest extends OrgzlyTest { private static final String DROPBOX_TEST_DIR = "/orgzly-android-tests"; @@ -27,18 +28,12 @@ public class DropboxRepoTest extends OrgzlyTest { @Before public void setUp() throws Exception { super.setUp(); - Assume.assumeTrue(BuildConfig.IS_DROPBOX_ENABLED); - Assume.assumeTrue(BuildConfig.DROPBOX_APP_KEY.length() > 0); - Assume.assumeTrue(BuildConfig.DROPBOX_REFRESH_TOKEN.length() > 0); - - JSONObject mockSerializedDbxCredential = new 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(context, mockSerializedDbxCredential.toString()); + testUtils.dropboxTestPreflight(); } + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + @Test public void testUrl() { assertEquals( @@ -75,6 +70,53 @@ public void testRenameBook() throws IOException { 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()); @@ -98,6 +140,14 @@ public void testDropboxFileRename() throws IOException { assertEquals("notebook-renamed.org", BookName.getInstance(context, repo.getBooks().get(0)).getFileName()); } + 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 new file mode 100644 index 000000000..3f4b1a260 --- /dev/null +++ b/app/src/androidTest/java/com/orgzly/android/repos/GitRepoTest.kt @@ -0,0 +1,152 @@ +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/RepoIgnoreNodeTest.kt b/app/src/androidTest/java/com/orgzly/android/repos/RepoIgnoreNodeTest.kt new file mode 100644 index 000000000..7f983087f --- /dev/null +++ b/app/src/androidTest/java/com/orgzly/android/repos/RepoIgnoreNodeTest.kt @@ -0,0 +1,47 @@ +package com.orgzly.android.repos + +import com.orgzly.android.OrgzlyTest +import com.orgzly.android.db.entity.Repo +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.FileNotFoundException +import java.io.InputStream +import java.util.HashMap + +class RepoIgnoreNodeTest : OrgzlyTest() { + + class MockRepoWithMockIgnoreFile : MockRepo(repoWithProps, null) { + override fun openRepoFileInputStream(filePath: String): InputStream { + if (filePath == RepoIgnoreNode.IGNORE_FILE) { + val ignoreFileContents = """ + IgnoredAnywhere.org + /OnlyIgnoredInRoot.org + CompletelyExcludedFolder/ + PartiallyExcludedFolder/** + !PartiallyExcludedFolder/included-*.org + """.trimIndent() + return ByteArrayInputStream(ignoreFileContents.toByteArray()) + } else { + throw FileNotFoundException() + } + } + } + + @Test + fun testIsPathIgnoredSyntax() { + val repo = MockRepoWithMockIgnoreFile() + val ignores = RepoIgnoreNode(repo) + assertEquals(true, ignores.isPathIgnored("IgnoredAnywhere.org", false)) + assertEquals(true, ignores.isPathIgnored("SomeFolder/IgnoredAnywhere.org", false)) + assertEquals(true, ignores.isPathIgnored("OnlyIgnoredInRoot.org", false)) + assertEquals(false, ignores.isPathIgnored("SomeFolder/OnlyIgnoredInRoot.org", false)) + assertEquals(true, ignores.isPathIgnored("CompletelyExcludedFolder/file.org", false)) + assertEquals(true, ignores.isPathIgnored("PartiallyExcludedFolder/whatever.org", false)) + assertEquals(false, ignores.isPathIgnored("PartiallyExcludedFolder/included-file.org", false)) + } + + companion object { + private val repoWithProps = RepoWithProps(Repo(0, RepoType.MOCK, "mock://repo"), HashMap()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/orgzly/android/git/GitPreferencesFromRepoPrefs.java b/app/src/main/java/com/orgzly/android/git/GitPreferencesFromRepoPrefs.java index d1468add8..8d043356e 100644 --- a/app/src/main/java/com/orgzly/android/git/GitPreferencesFromRepoPrefs.java +++ b/app/src/main/java/com/orgzly/android/git/GitPreferencesFromRepoPrefs.java @@ -16,13 +16,15 @@ public GitPreferencesFromRepoPrefs(RepoPreferences prefs) { @Override public GitTransportSetter createTransportSetter() { String scheme = remoteUri().getScheme(); - if ("https".equals(scheme)) { - String username = repoPreferences.getStringValue(R.string.pref_key_git_https_username, ""); - String password = repoPreferences.getStringValue(R.string.pref_key_git_https_password, ""); - return new HTTPSTransportSetter(username, password); - } else { - // assume SSH, since ssh:// usually isn't specified as the scheme when cloning via SSH. - return new GitSshKeyTransportSetter(); + switch (scheme) { + case "https": + String username = repoPreferences.getStringValue(R.string.pref_key_git_https_username, ""); + String password = repoPreferences.getStringValue(R.string.pref_key_git_https_password, ""); + return new HTTPSTransportSetter(username, password); + case "file": + return tc -> tc; + default: + return new GitSshKeyTransportSetter(); } } diff --git a/app/src/main/java/com/orgzly/android/prefs/AppPreferences.java b/app/src/main/java/com/orgzly/android/prefs/AppPreferences.java index 625b1f557..99015bc6b 100644 --- a/app/src/main/java/com/orgzly/android/prefs/AppPreferences.java +++ b/app/src/main/java/com/orgzly/android/prefs/AppPreferences.java @@ -936,6 +936,10 @@ public static boolean gitIsEnabled(Context context) { context.getResources().getString(R.string.pref_key_git_is_enabled), context.getResources().getBoolean(R.bool.pref_default_git_is_enabled)); } + + public static void gitIsEnabled(Context context, Boolean value) { + getDefaultSharedPreferences(context).edit().putBoolean(context.getResources().getString(R.string.pref_key_git_is_enabled), value).apply(); + } public static String defaultRepositoryStorageDirectory(Context context) { File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); 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 63c6f93e5..9145c9a3d 100644 --- a/app/src/main/java/com/orgzly/android/repos/MockRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/MockRepo.java @@ -7,6 +7,7 @@ import com.orgzly.android.data.DbRepoBookRepository; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.List; @@ -59,7 +60,7 @@ public VersionedRook retrieveBook(String fileName, File file) throws IOException @Override public InputStream openRepoFileInputStream(String fileName) throws IOException { - throw new UnsupportedOperationException("Not implemented"); + throw new FileNotFoundException(); } @Override From 4f7429889a4cac124fea542c88b10fb00bb438ac Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Sun, 9 Jun 2024 19:19:08 +0200 Subject: [PATCH 09/10] Add a few more sleeps to flaky Espresso tests --- .../androidTest/java/com/orgzly/android/espresso/BookTest.java | 1 + .../java/com/orgzly/android/espresso/QueryFragmentTest.java | 2 ++ .../java/com/orgzly/android/espresso/util/EspressoUtils.java | 1 - 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/BookTest.java b/app/src/androidTest/java/com/orgzly/android/espresso/BookTest.java index e94588c37..532959c55 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/BookTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/BookTest.java @@ -444,6 +444,7 @@ public void testSetDeadlineTimeForNewNote() { onView(withId(R.id.date_picker_button)).perform(click()); onView(withClassName(equalTo(DatePicker.class.getName()))).perform(setDate(2014, 4, 1)); onView(withText(android.R.string.ok)).perform(click()); + SystemClock.sleep(100); onView(withText(R.string.set)).perform(click()); onView(withId(R.id.deadline_button)).check(matches(withText(userDateTime("<2014-04-01 Tue>")))); } 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 d9d6c95fc..0afbd5ef3 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/QueryFragmentTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/QueryFragmentTest.java @@ -246,6 +246,7 @@ public void testClickingNote() { defaultSetUp(); onView(allOf(withText("book-two"), isDisplayed())).perform(click()); + SystemClock.sleep(200); searchForTextCloseKeyboard("b.book-two Note"); onView(withId(R.id.fragment_query_search_view_flipper)).check(matches(isDisplayed())); onNotesInSearch().check(matches(recyclerViewItemCount(29))); @@ -615,6 +616,7 @@ public void testSearchForTagOrTag() { scenario = ActivityScenario.launch(MainActivity.class); onView(allOf(withText("notebook"), isDisplayed())).perform(click()); + SystemClock.sleep(200); searchForTextCloseKeyboard("tn.a or tn.b"); onView(withId(R.id.fragment_query_search_view_flipper)).check(matches(isDisplayed())); onNotesInSearch().check(matches(recyclerViewItemCount(2))); 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 88ac1912f..cc401eaba 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 @@ -341,7 +341,6 @@ public static ViewInteraction contextualToolbarOverflowMenu() { } public static void searchForTextCloseKeyboard(String str) { - SystemClock.sleep(100); onView(isRoot()).perform(waitId(R.id.search_view, 5000)); onView(allOf(withId(R.id.search_view), isDisplayed())).perform(click()); onView(isRoot()).perform(waitId(R.id.search_src_text, 5000)); From 33d1f7bc88cbd1b8bd102e1fd382d33dd8fd30a1 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Fri, 2 Aug 2024 14:34:25 +0200 Subject: [PATCH 10/10] Release 1.8.25 --- app/build.gradle | 4 ++-- app/src/main/res/layout/dialog_whats_new.xml | 2 +- metadata/en-US/changelogs/214.txt | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 metadata/en-US/changelogs/214.txt diff --git a/app/build.gradle b/app/build.gradle index 8d3c04500..27beb38f4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 33 - versionCode 213 - versionName "1.8.25-beta.1" + versionCode 214 + versionName "1.8.25" applicationId = "com.orgzlyrevived" resValue "string", "application_id", "com.orgzlyrevived" diff --git a/app/src/main/res/layout/dialog_whats_new.xml b/app/src/main/res/layout/dialog_whats_new.xml index f36621931..1cb332d59 100644 --- a/app/src/main/res/layout/dialog_whats_new.xml +++ b/app/src/main/res/layout/dialog_whats_new.xml @@ -31,7 +31,7 @@ + app:text="Support selecting multiple notebooks" />