diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/ShareActivityTest.kt b/app/src/androidTest/java/com/orgzly/android/espresso/ShareActivityTest.kt
index 720070e89..743809700 100644
--- a/app/src/androidTest/java/com/orgzly/android/espresso/ShareActivityTest.kt
+++ b/app/src/androidTest/java/com/orgzly/android/espresso/ShareActivityTest.kt
@@ -13,6 +13,7 @@ import com.orgzly.R
import com.orgzly.android.AppIntent
import com.orgzly.android.OrgzlyTest
import com.orgzly.android.espresso.util.EspressoUtils.*
+import com.orgzly.android.prefs.AppPreferences
import com.orgzly.android.ui.share.ShareActivity
import org.hamcrest.Matchers.startsWith
import org.junit.Assert.assertTrue
@@ -156,17 +157,25 @@ class ShareActivityTest : OrgzlyTest() {
extraStreamUri = "content://uri")
onView(withId(R.id.title_view)).check(matches(withText("content://uri")))
- onView(withId(R.id.content_view)).check(matches(withText("Cannot find image using this URI.")))
+ onView(withId(R.id.content_view)).check(matches(withText("content://uri\n" +
+ "\n" +
+ "Cannot determine fileName to this content.")))
onView(withId(R.id.done)).perform(click()); // Note done
}
@Test
- fun testNoMatchingType() {
- startActivityWithIntent(action = Intent.ACTION_SEND, type = "application/octet-stream")
+ fun testFileCopy() {
+ AppPreferences.attachMethod(context, ShareActivity.ATTACH_METHOD_COPY_DIR);
+ startActivityWithIntent(
+ action = Intent.ACTION_SEND,
+ type = "application/pdf",
+ extraStreamUri = "content://uri")
- onView(withId(R.id.title_view)).check(matches(withText("")))
- onSnackbar().check(matches(withText(context.getString(R.string.share_type_not_supported, "application/octet-stream"))))
+ onView(withId(R.id.title_view)).check(matches(withText("content://uri")))
+ onView(withId(R.id.content_view)).check(matches(withText("content://uri\n\nCannot determine fileName to this content.")))
+
+ onView(withId(R.id.done)).perform(click()) // Note done
}
@Test
diff --git a/app/src/androidTest/java/com/orgzly/android/util/OrgFormatterLinkTest.kt b/app/src/androidTest/java/com/orgzly/android/util/OrgFormatterLinkTest.kt
index dcfe67e01..f729df672 100644
--- a/app/src/androidTest/java/com/orgzly/android/util/OrgFormatterLinkTest.kt
+++ b/app/src/androidTest/java/com/orgzly/android/util/OrgFormatterLinkTest.kt
@@ -1,5 +1,7 @@
package com.orgzly.android.util
+import android.text.style.URLSpan
+import com.orgzly.android.ui.views.style.AttachmentLinkSpan
import com.orgzly.android.ui.views.style.FileLinkSpan
import com.orgzly.android.ui.views.style.FileOrNotLinkSpan
import com.orgzly.android.ui.views.style.IdLinkSpan
@@ -67,6 +69,10 @@ class OrgFormatterLinkTest(private val param: Parameter) : OrgFormatterTest() {
Parameter("[[file:orgzly-tests/document.txt]]", "file:orgzly-tests/document.txt", listOf(Span(0, 30, FileLinkSpan::class.java))),
Parameter("[[file:orgzly-tests/document.txt][Document]]", "Document", listOf(Span(0, 8, FileLinkSpan::class.java))),
+ Parameter("attachment:orgzly-tests/document.txt", "attachment:orgzly-tests/document.txt", listOf(Span(0, 36, AttachmentLinkSpan::class.java))),
+ Parameter("[[attachment:orgzly-tests/document.txt]]", "attachment:orgzly-tests/document.txt", listOf(Span(0, 36, AttachmentLinkSpan::class.java))),
+ Parameter("[[attachment:orgzly-tests/document.txt][Document]]", "Document", listOf(Span(0, 8, AttachmentLinkSpan::class.java))),
+
Parameter("id:45DFE015-255E-4B86-B957-F7FD77364DCA", "id:45DFE015-255E-4B86-B957-F7FD77364DCA", listOf(Span(0, 39, IdLinkSpan::class.java))),
Parameter("[[id:45DFE015-255E-4B86-B957-F7FD77364DCA]]", "id:45DFE015-255E-4B86-B957-F7FD77364DCA", listOf(Span(0, 39, IdLinkSpan::class.java))),
Parameter("id:foo", "id:foo", listOf(Span(0, 6, IdLinkSpan::class.java))),
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f272521df..2f6ffaabf 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -79,7 +79,7 @@
-
+
+ tempFile = getTempBookFile()
+ MiscUtils.writeStreamToFile(inputStream, tempFile)
+ LogUtils.d(TAG, "Wrote to file $tempFile")
+ }
+
+ repo.storeFile(tempFile, attachDir, fileName)
+ LogUtils.d(TAG, "Stored file to repo")
+ tempFile.delete()
+ }
+
/**
* Loads book from resource.
*/
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 6324fd1a2..e557ef683 100644
--- a/app/src/main/java/com/orgzly/android/prefs/AppPreferences.java
+++ b/app/src/main/java/com/orgzly/android/prefs/AppPreferences.java
@@ -696,6 +696,29 @@ public static String fileRelativeRoot(Context context) {
);
}
+ public static String attachMethod(Context context) {
+ return getDefaultSharedPreferences(context).getString(
+ context.getResources().getString(R.string.pref_key_attach_method),
+ context.getResources().getString(R.string.pref_default_attach_method));
+ }
+
+ public static void attachMethod(Context context, String value) {
+ String key = context.getResources().getString(R.string.pref_key_attach_method);
+ getDefaultSharedPreferences(context).edit().putString(key, value).apply();
+ }
+
+ /**
+ * When attachMethod is `link`, this pref is not used for saving attachment.
+ * When attachMethod is `copy_dir`, this pref is the target for saving attachment.
+ * When attachMethod is `copy_id`, this pref is used as a prefix for saving attachment, used
+ * together with ID subdirectory.
+ */
+ public static String attachDirDefaultPath(Context context) {
+ return getDefaultSharedPreferences(context).getString(
+ context.getResources().getString(R.string.pref_key_attach_dir_default_path),
+ "data");
+ }
+
/*
* Note's metadata visibility
*/
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..cffde35ee 100644
--- a/app/src/main/java/com/orgzly/android/repos/ContentRepo.java
+++ b/app/src/main/java/com/orgzly/android/repos/ContentRepo.java
@@ -128,18 +128,25 @@ public VersionedRook retrieveBook(String fileName, File destinationFile) throws
@Override
public VersionedRook storeBook(File file, String fileName) throws IOException {
+ return storeFile(file, "", fileName);
+ }
+
+ @Override
+ public VersionedRook storeFile(File file, String pathInRepo, String fileName) throws IOException {
if (!file.exists()) {
throw new FileNotFoundException("File " + file + " does not exist");
}
+ DocumentFile documentFile = createRecursive(repoDocumentFile, pathInRepo);
+
/* Delete existing file. */
- DocumentFile existingFile = repoDocumentFile.findFile(fileName);
+ DocumentFile existingFile = documentFile.findFile(fileName);
if (existingFile != null) {
existingFile.delete();
}
/* Create new file. */
- DocumentFile destinationFile = repoDocumentFile.createFile("text/*", fileName);
+ DocumentFile destinationFile = documentFile.createFile("text/*", fileName);
if (destinationFile == null) {
throw new IOException("Failed creating " + fileName + " in " + repoUri);
@@ -163,6 +170,27 @@ public VersionedRook storeBook(File file, String fileName) throws IOException {
return new VersionedRook(repoId, RepoType.DOCUMENT, getUri(), uri, rev, mtime);
}
+ private DocumentFile createRecursive(DocumentFile parent, String path) {
+ if (".".equals(path) || "".equals(path)) {
+ return parent;
+ }
+ int l = path.lastIndexOf('/');
+ DocumentFile p;
+ if (l >= 0) {
+ p = createRecursive(parent, path.substring(0, l));
+ } else {
+ p = parent;
+ }
+ String subdir = path.substring(l+1);
+ DocumentFile f = p.findFile(subdir);
+ if (f != null) {
+ // already exist, return it
+ return f;
+ }
+ // Otherwise, create the directory
+ return p.createDirectory(subdir);
+ }
+
@Override
public VersionedRook renameBook(Uri from, String name) throws IOException {
DocumentFile fromDocFile = DocumentFile.fromSingleUri(context, from);
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..15eabe610 100644
--- a/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java
+++ b/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java
@@ -66,6 +66,11 @@ public VersionedRook storeBook(File file, String fileName) throws IOException {
return dbRepo.createBook(repoId, vrook, content);
}
+ @Override
+ public VersionedRook storeFile(File file, String pathInRepo, String fileName) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public VersionedRook renameBook(Uri fromUri, String name) {
Uri toUri = UriUtils.getUriForNewName(fromUri, name);
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..8d5f4d224 100644
--- a/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java
+++ b/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java
@@ -158,6 +158,11 @@ public VersionedRook storeBook(File file, String fileName) throws IOException {
return new VersionedRook(repoId, RepoType.DIRECTORY, repoUri, uri, rev, mtime);
}
+ @Override
+ public VersionedRook storeFile(File file, String pathInRepo, String fileName) throws IOException {
+ return storeBook(file, pathInRepo + File.separator + fileName);
+ }
+
@Override
public VersionedRook renameBook(Uri fromUri, String name) throws IOException {
String fromFilePath = fromUri.getPath();
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 b69949ba2..090258f8a 100644
--- a/app/src/main/java/com/orgzly/android/repos/DropboxClient.java
+++ b/app/src/main/java/com/orgzly/android/repos/DropboxClient.java
@@ -19,6 +19,7 @@
import com.orgzly.BuildConfig;
import com.orgzly.android.BookName;
import com.orgzly.android.prefs.AppPreferences;
+import com.orgzly.android.util.LogUtils;
import java.io.BufferedOutputStream;
import java.io.File;
@@ -190,7 +191,7 @@ public List getBooks(Uri repoUri) throws IOException {
}
} catch (DbxException e) {
- e.printStackTrace();
+ LogUtils.d(TAG, e.toString());
/* If we get NOT_FOUND from Dropbox, just return the empty list. */
if (e instanceof GetMetadataErrorException) {
@@ -235,6 +236,7 @@ public VersionedRook download(Uri repoUri, String fileName, File localFile) thro
}
} catch (DbxException e) {
+ LogUtils.d(TAG, e.toString());
if (e.getMessage() != null) {
throw new IOException("Failed downloading Dropbox file " + uri + ": " + e.getMessage());
} else {
@@ -266,6 +268,7 @@ public VersionedRook upload(File file, Uri repoUri, String fileName) throws IOEx
.uploadAndFinish(in);
} catch (DbxException e) {
+ LogUtils.d(TAG, e.toString());
if (e.getMessage() != null) {
throw new IOException("Failed overwriting " + bookUri.getPath() + " on Dropbox: " + e.getMessage());
} else {
@@ -290,7 +293,7 @@ public void delete(String path) throws IOException {
}
} catch (DbxException e) {
- e.printStackTrace();
+ LogUtils.d(TAG, e.toString());
if (e.getMessage() != null) {
throw new IOException("Failed deleting " + path + " on Dropbox: " + e.getMessage());
@@ -319,7 +322,7 @@ public VersionedRook move(Uri repoUri, Uri from, Uri to) throws IOException {
return new VersionedRook(repoId, RepoType.DROPBOX, repoUri, to, rev, mtime);
} catch (Exception e) {
- e.printStackTrace();
+ LogUtils.d(TAG, e.toString());
if (e.getMessage() != null) { // TODO: Move this throwing to utils
throw new IOException("Failed moving " + from + " to " + to + ": " + e.getMessage(), 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
deleted file mode 100644
index 484da45a6..000000000
--- a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.orgzly.android.repos;
-
-import android.content.Context;
-import android.net.Uri;
-
-import com.orgzly.android.util.UriUtils;
-
-import java.io.File;
-import java.io.IOException;
-
-import java.util.List;
-
-public class DropboxRepo implements SyncRepo {
- public static final String SCHEME = "dropbox";
-
- private final Uri repoUri;
- private final DropboxClient client;
-
- public DropboxRepo(RepoWithProps repoWithProps, Context context) {
- this.repoUri = Uri.parse(repoWithProps.getRepo().getUrl());
- this.client = new DropboxClient(context, repoWithProps.getRepo().getId());
- }
-
- @Override
- public boolean isConnectionRequired() {
- return true;
- }
-
- @Override
- public boolean isAutoSyncSupported() {
- return false;
- }
-
- @Override
- public Uri getUri() {
- return repoUri;
- }
-
- @Override
- public List getBooks() throws IOException {
- return client.getBooks(repoUri);
- }
-
- @Override
- public VersionedRook retrieveBook(String fileName, File file) throws IOException {
- return client.download(repoUri, fileName, file);
- }
-
- @Override
- public VersionedRook storeBook(File file, String fileName) throws IOException {
- return client.upload(file, repoUri, fileName);
- }
-
- @Override
- public VersionedRook renameBook(Uri fromUri, String name) throws IOException {
- Uri toUri = UriUtils.getUriForNewName(fromUri, name);
- return client.move(repoUri, fromUri, toUri);
- }
-
- @Override
- public void delete(Uri uri) throws IOException {
- client.delete(uri.getPath());
- }
-
- @Override
- public String toString() {
- return repoUri.toString();
- }
-}
diff --git a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.kt b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.kt
new file mode 100644
index 000000000..6184e20d2
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.kt
@@ -0,0 +1,74 @@
+package com.orgzly.android.repos
+
+import android.content.Context
+import android.net.Uri
+import kotlin.Throws
+import com.orgzly.android.util.UriUtils
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.IOException
+
+class DropboxRepo(repoWithProps: RepoWithProps, context: Context?) : SyncRepo {
+ private val repoUri: Uri
+ private val client: DropboxClient
+ override fun isConnectionRequired(): Boolean {
+ return true
+ }
+
+ override fun isAutoSyncSupported(): Boolean {
+ return false
+ }
+
+ override fun getUri(): Uri {
+ return repoUri
+ }
+
+ @Throws(IOException::class)
+ override fun getBooks(): List {
+ return client.getBooks(repoUri)
+ }
+
+ @Throws(IOException::class)
+ override fun retrieveBook(fileName: String, file: File): VersionedRook {
+ return client.download(repoUri, fileName, file)
+ }
+
+ @Throws(IOException::class)
+ override fun storeBook(file: File, fileName: String): VersionedRook {
+ return client.upload(file, repoUri, fileName)
+ }
+
+ @Throws(IOException::class)
+ override fun storeFile(file: File, pathInRepo: String, fileName: String): VersionedRook {
+ if (file == null || !file.exists()) {
+ throw FileNotFoundException("File $file does not exist")
+ }
+
+ val folderUri = Uri.withAppendedPath(uri, pathInRepo)
+ return client.upload(file, folderUri, fileName)
+ }
+
+ @Throws(IOException::class)
+ override fun renameBook(fromUri: Uri, name: String): VersionedRook {
+ val toUri = UriUtils.getUriForNewName(fromUri, name)
+ return client.move(repoUri, fromUri, toUri)
+ }
+
+ @Throws(IOException::class)
+ override fun delete(uri: Uri) {
+ client.delete(uri.path)
+ }
+
+ override fun toString(): String {
+ return repoUri.toString()
+ }
+
+ companion object {
+ const val SCHEME = "dropbox"
+ }
+
+ init {
+ repoUri = Uri.parse(repoWithProps.repo.url)
+ client = DropboxClient(context, repoWithProps.repo.id)
+ }
+}
\ No newline at end of file
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 b7f66efcf..1315d2f3a 100644
--- a/app/src/main/java/com/orgzly/android/repos/GitRepo.java
+++ b/app/src/main/java/com/orgzly/android/repos/GitRepo.java
@@ -195,6 +195,11 @@ public VersionedRook storeBook(File file, String fileName) throws IOException {
return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(fileName).build());
}
+ @Override
+ public VersionedRook storeFile(File file, String pathInRepo, String fileName) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
private RevWalk walk() {
return new RevWalk(git.getRepository());
}
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..7b295b850 100644
--- a/app/src/main/java/com/orgzly/android/repos/MockRepo.java
+++ b/app/src/main/java/com/orgzly/android/repos/MockRepo.java
@@ -62,6 +62,12 @@ public VersionedRook storeBook(File file, String fileName) throws IOException {
return databaseRepo.storeBook(file, fileName);
}
+ @Override
+ public VersionedRook storeFile(File file, String pathInRepo, String fileName) throws IOException {
+ SystemClock.sleep(SLEEP_FOR_STORE_BOOK);
+ return databaseRepo.storeFile(file, pathInRepo, fileName);
+ }
+
@Override
public VersionedRook renameBook(Uri fromUri, String name) throws IOException {
SystemClock.sleep(SLEEP_FOR_STORE_BOOK);
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..1fc824d40 100644
--- a/app/src/main/java/com/orgzly/android/repos/SyncRepo.java
+++ b/app/src/main/java/com/orgzly/android/repos/SyncRepo.java
@@ -39,6 +39,16 @@ public interface SyncRepo {
*/
VersionedRook storeBook(File file, String fileName) throws IOException;
+ /**
+ * Uploads file storing it under directory (pathInRepo) under repo's url.
+ * @param file The contents of this file should be stored at the remote location/repo
+ * @param pathInRepo The "/" separated path within the remote location/repo, create it if it doesn't exist
+ * @param fileName The contents ({@code file}) should be stored under this name
+ * @return {@code VersionedRook}
+ * @throws IOException
+ */
+ VersionedRook storeFile(File file, String pathInRepo, String fileName) throws IOException;
+
VersionedRook renameBook(Uri from, String name) 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 e8bcd01e5..a774be92f 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
@@ -194,6 +195,39 @@ class WebdavRepo(
return sardine.list(fileUrl).first().toVersionedRook()
}
+ override fun storeFile(file: File?, pathInRepo: String?, fileName: String?): VersionedRook {
+ if (file == null || !file.exists()) {
+ throw FileNotFoundException("File $file does not exist")
+ }
+
+ val folderUri = Uri.withAppendedPath(uri, pathInRepo)
+ val fileUrl = Uri.withAppendedPath(folderUri, fileName).toUrl()
+
+ createRecursive(uri.toUrl(), pathInRepo!!)
+
+ sardine.put(fileUrl, file, null)
+
+ return sardine.list(fileUrl).first().toVersionedRook()
+ }
+
+ private fun createRecursive(parent: String, path: String): String {
+ if ("." == path || "" == path) {
+ return parent
+ }
+ val l = path.lastIndexOf('/')
+ val p = if (l >= 0) {
+ createRecursive(parent, path.substring(0, l))
+ } else {
+ parent
+ }
+ val subdir = path.substring(l + 1)
+ val folder = p + "/" + subdir
+ if (!sardine.exists(folder)) {
+ sardine.createDirectory(folder)
+ }
+ return folder
+ }
+
override fun renameBook(from: Uri, name: String?): VersionedRook {
val destUrl = UriUtils.getUriForNewName(from, name).toUrl()
sardine.move(from.toUrl(), destUrl)
diff --git a/app/src/main/java/com/orgzly/android/ui/AttachmentSpanLoader.kt b/app/src/main/java/com/orgzly/android/ui/AttachmentSpanLoader.kt
new file mode 100644
index 000000000..982ead381
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/ui/AttachmentSpanLoader.kt
@@ -0,0 +1,19 @@
+package com.orgzly.android.ui
+
+import android.text.Spannable
+import com.orgzly.android.ui.views.richtext.RichText
+import com.orgzly.android.ui.views.style.AttachmentLinkSpan
+import com.orgzly.android.usecase.FindAttachmentPath
+import com.orgzly.android.usecase.UseCaseRunner
+
+object AttachmentSpanLoader {
+ /** Find all `attachment:` links and set up the prefix directory based on `ID` property. */
+ fun loadAttachmentPaths(noteId: Long, richText: RichText) {
+ SpanUtils.forEachSpan(richText.getVisibleText() as Spannable, AttachmentLinkSpan::class.java) { span, _, _ ->
+ val prefix = UseCaseRunner.run(FindAttachmentPath(noteId)).userData
+ if (prefix != null) {
+ span.prefix = prefix as String
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/orgzly/android/ui/ImageLoader.kt b/app/src/main/java/com/orgzly/android/ui/ImageLoader.kt
index 536d5a3b5..9ed1b0c61 100644
--- a/app/src/main/java/com/orgzly/android/ui/ImageLoader.kt
+++ b/app/src/main/java/com/orgzly/android/ui/ImageLoader.kt
@@ -7,6 +7,7 @@ import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.text.Spannable
+import android.text.style.ClickableSpan
import android.text.style.ImageSpan
import android.view.View
import android.widget.TextView
@@ -21,6 +22,7 @@ import com.orgzly.R
import com.orgzly.android.App
import com.orgzly.android.prefs.AppPreferences
import com.orgzly.android.ui.views.style.FileLinkSpan
+import com.orgzly.android.ui.views.style.AttachmentLinkSpan
import com.orgzly.android.usecase.LinkFindTarget
import com.orgzly.android.usecase.UseCaseRunner
import com.orgzly.android.util.AppPermissions
@@ -39,14 +41,16 @@ object ImageLoader {
&& AppPermissions.isGranted(context, AppPermissions.Usage.EXTERNAL_FILES_ACCESS)) {
// Load the associated image for each FileLinkSpan
SpanUtils.forEachSpan(textWithMarkup.text as Spannable, FileLinkSpan::class.java) { span, _, _ ->
- loadImage(textWithMarkup, span)
+ loadImage(textWithMarkup, span, span.path)
+ }
+ // Load the associated image for each AttachmentLinkSpan
+ SpanUtils.forEachSpan(textWithMarkup.text as Spannable, AttachmentLinkSpan::class.java) { span, _, _ ->
+ loadImage(textWithMarkup, span, span.getPrefixedPath())
}
}
}
- private fun loadImage(textWithMarkup: TextView, fileLinkSpan: FileLinkSpan) {
- val path = fileLinkSpan.path
-
+ private fun loadImage(textWithMarkup: TextView, fileLinkSpan: ClickableSpan, path: String) {
if (hasSupportedExtension(path)) {
val text = textWithMarkup.text as Spannable
// Get the current context
diff --git a/app/src/main/java/com/orgzly/android/ui/note/NoteBuilder.kt b/app/src/main/java/com/orgzly/android/ui/note/NoteBuilder.kt
index 16bd1b888..a21db4fa2 100644
--- a/app/src/main/java/com/orgzly/android/ui/note/NoteBuilder.kt
+++ b/app/src/main/java/com/orgzly/android/ui/note/NoteBuilder.kt
@@ -1,6 +1,7 @@
package com.orgzly.android.ui.note
import android.content.Context
+import android.net.Uri
import com.orgzly.android.db.entity.NoteProperty
import com.orgzly.android.db.entity.NoteView
import com.orgzly.android.prefs.AppPreferences
@@ -99,7 +100,7 @@ class NoteBuilder {
}
@JvmStatic
- fun newPayload(context: Context, title: String, content: String?): NotePayload {
+ fun newPayload(context: Context, title: String, content: String?, attachmentUri: Uri?): NotePayload {
val scheduled = initialScheduledTime(context)
@@ -109,7 +110,8 @@ class NoteBuilder {
title = title,
content = content,
state = state,
- scheduled = scheduled
+ scheduled = scheduled,
+ attachmentUri = attachmentUri
)
}
diff --git a/app/src/main/java/com/orgzly/android/ui/note/NoteFragment.kt b/app/src/main/java/com/orgzly/android/ui/note/NoteFragment.kt
index aaf4cf65b..b59bbd645 100644
--- a/app/src/main/java/com/orgzly/android/ui/note/NoteFragment.kt
+++ b/app/src/main/java/com/orgzly/android/ui/note/NoteFragment.kt
@@ -3,6 +3,7 @@ package com.orgzly.android.ui.note
import android.content.Context
import android.content.Intent
import android.graphics.Typeface
+import android.net.Uri
import android.os.Bundle
import android.text.Editable
import android.text.TextUtils
@@ -106,8 +107,9 @@ class NoteFragment : Fragment(), View.OnClickListener, TimestampDialogFragment.O
// Initial values when sharing
val title = args.getString(ARG_TITLE)
val content = args.getString(ARG_CONTENT)
+ val attachmentUri = args.getString(ARG_ATTACHMENT_URI)?.let { Uri.parse(it) }
- return NoteInitialData(bookId, noteId, place, title, content)
+ return NoteInitialData(bookId, noteId, place, title, content, attachmentUri)
}
}
@@ -451,6 +453,7 @@ class NoteFragment : Fragment(), View.OnClickListener, TimestampDialogFragment.O
addPropertyToList(null, null)
// Content
+ binding.content.noteId = viewModel.noteId
binding.content.setSourceText(payload.content)
}
@@ -1083,21 +1086,24 @@ class NoteFragment : Fragment(), View.OnClickListener, TimestampDialogFragment.O
private const val ARG_PLACE = "place"
private const val ARG_TITLE = "title"
private const val ARG_CONTENT = "content"
+ private const val ARG_ATTACHMENT_URI = "attachment_uri"
@JvmStatic
@JvmOverloads
fun forNewNote(
- notePlace: NotePlace,
- initialTitle: String? = null,
- initialContent: String? = null): NoteFragment? {
+ notePlace: NotePlace,
+ initialTitle: String? = null,
+ initialContent: String? = null,
+ attachmentUri: Uri? = null): NoteFragment? {
return if (notePlace.bookId > 0) {
getInstance(
- notePlace.bookId,
- notePlace.noteId,
- notePlace.place,
- initialTitle,
- initialContent)
+ notePlace.bookId,
+ notePlace.noteId,
+ notePlace.place,
+ initialTitle,
+ initialContent,
+ attachmentUri)
} else {
Log.e(TAG, "Invalid book id ${notePlace.bookId}")
null
@@ -1116,11 +1122,12 @@ class NoteFragment : Fragment(), View.OnClickListener, TimestampDialogFragment.O
@JvmStatic
private fun getInstance(
- bookId: Long,
- noteId: Long,
- place: Place? = null,
- initialTitle: String? = null,
- initialContent: String? = null): NoteFragment {
+ bookId: Long,
+ noteId: Long,
+ place: Place? = null,
+ initialTitle: String? = null,
+ initialContent: String? = null,
+ attachmentUri: Uri? = null): NoteFragment {
val fragment = NoteFragment()
@@ -1144,6 +1151,10 @@ class NoteFragment : Fragment(), View.OnClickListener, TimestampDialogFragment.O
args.putString(ARG_CONTENT, initialContent)
}
+ if (attachmentUri != null) {
+ args.putString(ARG_ATTACHMENT_URI, attachmentUri.toString())
+ }
+
fragment.arguments = args
return fragment
diff --git a/app/src/main/java/com/orgzly/android/ui/note/NotePayload.kt b/app/src/main/java/com/orgzly/android/ui/note/NotePayload.kt
index dbd07f01f..1796ba574 100644
--- a/app/src/main/java/com/orgzly/android/ui/note/NotePayload.kt
+++ b/app/src/main/java/com/orgzly/android/ui/note/NotePayload.kt
@@ -1,7 +1,12 @@
package com.orgzly.android.ui.note
+import android.content.Context
+import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
+import com.orgzly.android.prefs.AppPreferences
+import com.orgzly.android.ui.share.ShareActivity
+import com.orgzly.android.util.AttachmentUtils
import com.orgzly.org.OrgProperties
data class NotePayload @JvmOverloads constructor(
@@ -13,7 +18,8 @@ data class NotePayload @JvmOverloads constructor(
val deadline: String? = null,
val closed: String? = null,
val tags: List = emptyList(),
- val properties: OrgProperties = OrgProperties()
+ val properties: OrgProperties = OrgProperties(),
+ val attachmentUri: Uri? = null
) : Parcelable {
override fun describeContents(): Int {
@@ -40,6 +46,22 @@ data class NotePayload @JvmOverloads constructor(
out.writeString(property.value)
}
}
+
+ out.writeString(attachmentUri.toString())
+ }
+
+ /** Returns the path to store the attachment. */
+ fun attachDir(context: Context): String {
+ val idStr = properties.get("ID")
+ // TODO idStr could be null. Throw a warning exception, show a toast, don't attach anything
+ when(AppPreferences.attachMethod(context)) {
+ ShareActivity.ATTACH_METHOD_LINK -> return ""
+ ShareActivity.ATTACH_METHOD_COPY_DIR -> return AppPreferences.attachDirDefaultPath(context)
+ ShareActivity.ATTACH_METHOD_COPY_ID -> {
+ return AttachmentUtils.getAttachDir(context, idStr)
+ }
+ }
+ return ""
}
companion object {
@@ -69,6 +91,8 @@ data class NotePayload @JvmOverloads constructor(
properties.put(name!!, value!!)
}
+ val attachmentUri: Uri? = parcel.readString()?.let { Uri.parse(it) }
+
return NotePayload(
title!!,
content,
@@ -78,7 +102,8 @@ data class NotePayload @JvmOverloads constructor(
deadline,
closed,
tags,
- properties
+ properties,
+ attachmentUri
)
}
diff --git a/app/src/main/java/com/orgzly/android/ui/note/NoteViewModel.kt b/app/src/main/java/com/orgzly/android/ui/note/NoteViewModel.kt
index 362023848..a233d9de8 100644
--- a/app/src/main/java/com/orgzly/android/ui/note/NoteViewModel.kt
+++ b/app/src/main/java/com/orgzly/android/ui/note/NoteViewModel.kt
@@ -1,5 +1,6 @@
package com.orgzly.android.ui.note
+import android.net.Uri
import android.os.Bundle
import android.text.TextUtils
import androidx.lifecycle.LiveData
@@ -17,18 +18,21 @@ import com.orgzly.android.ui.NotePlace
import com.orgzly.android.ui.Place
import com.orgzly.android.ui.SingleLiveEvent
import com.orgzly.android.ui.main.MainActivity
+import com.orgzly.android.ui.share.ShareActivity
import com.orgzly.android.usecase.*
import com.orgzly.android.util.MiscUtils
import com.orgzly.org.OrgProperties
import com.orgzly.org.datetime.OrgRange
import com.orgzly.org.parser.OrgParserWriter
+import java.util.*
data class NoteInitialData(
val bookId: Long,
val noteId: Long, // Could be 0 if new note is being created
val place: Place? = null, // Relative location, used for new notes
val title: String? = null, // Initial title, used for when sharing
- val content: String? = null // Initial content, used for when sharing
+ val content: String? = null, // Initial content, used for when sharing
+ val attachmentUri: Uri? = null // Initial attachment Uri, used for when sharing
)
class NoteViewModel(
@@ -40,6 +44,7 @@ class NoteViewModel(
private val place = initialData.place
private val title = initialData.title
private val content = initialData.content
+ private val attachmentUri = initialData.attachmentUri
val bookView: MutableLiveData = MutableLiveData()
@@ -75,10 +80,14 @@ class NoteViewModel(
dataRepository.getNoteAncestors(noteId)
}
- notePayload = if (isNew()) {
- NoteBuilder.newPayload(App.getAppContext(), title.orEmpty(), content)
+ if (isNew()) {
+ notePayload = NoteBuilder.newPayload(App.getAppContext(), title.orEmpty(), content, attachmentUri)
+ // Auto generate ID property if it has attachment.
+ if (attachmentUri != null && AppPreferences.attachMethod(App.getAppContext()) == ShareActivity.ATTACH_METHOD_COPY_ID) {
+ updatePayloadCreateIdProperty()
+ }
} else {
- dataRepository.getNotePayload(noteId)
+ notePayload = dataRepository.getNotePayload(noteId)
}
// Calculate payload's hash once for the original note
@@ -164,6 +173,15 @@ class NoteViewModel(
notePayload = notePayload?.copy(closed = range?.toString())
}
+ private fun updatePayloadCreateIdProperty() {
+ if (notePayload?.properties!!.containsKey("ID")) {
+ return
+ }
+ notePayload = notePayload?.copy()
+ val idStr = UUID.randomUUID().toString().uppercase()
+ notePayload?.properties!!.put("ID", idStr)
+ }
+
private fun createNote(postSave: ((note: Note) -> Unit)?) {
val notePlace = if (place != Place.UNSPECIFIED)
NotePlace(bookId, noteId, place)
diff --git a/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt b/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt
index 9391f8263..bae67c49e 100644
--- a/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt
+++ b/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt
@@ -12,6 +12,7 @@ import com.orgzly.android.App
import com.orgzly.android.db.entity.Note
import com.orgzly.android.db.entity.NoteView
import com.orgzly.android.prefs.AppPreferences
+import com.orgzly.android.ui.AttachmentSpanLoader
import com.orgzly.android.ui.TimeType
import com.orgzly.android.ui.util.TitleGenerator
import com.orgzly.android.ui.util.styledAttributes
@@ -102,6 +103,7 @@ class NoteItemViewBinder(private val context: Context, private val inBook: Boole
holder.binding.itemHeadContent.setTypeface(Typeface.MONOSPACE)
}
+ holder.binding.itemHeadContent.noteId = note.id
holder.binding.itemHeadContent.setSourceText(note.content)
/* If content changes (for example by toggling the checkbox), update the note. */
diff --git a/app/src/main/java/com/orgzly/android/ui/share/ShareActivity.java b/app/src/main/java/com/orgzly/android/ui/share/ShareActivity.java
index c796f0e65..168882e03 100644
--- a/app/src/main/java/com/orgzly/android/ui/share/ShareActivity.java
+++ b/app/src/main/java/com/orgzly/android/ui/share/ShareActivity.java
@@ -13,6 +13,7 @@
import androidx.core.app.TaskStackBuilder;
import androidx.core.content.pm.ShortcutManagerCompat;
+import androidx.documentfile.provider.DocumentFile;
import com.orgzly.BuildConfig;
import com.orgzly.R;
@@ -22,6 +23,7 @@
import com.orgzly.android.db.entity.Book;
import com.orgzly.android.db.entity.Note;
import com.orgzly.android.db.entity.SavedSearch;
+import com.orgzly.android.prefs.AppPreferences;
import com.orgzly.android.query.Query;
import com.orgzly.android.query.QueryUtils;
import com.orgzly.android.query.user.DottedQueryParser;
@@ -37,6 +39,7 @@
import com.orgzly.android.usecase.UseCaseResult;
import com.orgzly.android.util.LogUtils;
import com.orgzly.android.util.MiscUtils;
+import com.orgzly.org.OrgStringUtils;
import java.io.File;
import java.io.IOException;
@@ -58,6 +61,10 @@ public class ShareActivity extends CommonActivity
public static final String TAG = ShareActivity.class.getName();
+ public static final String ATTACH_METHOD_LINK = "link";
+ public static final String ATTACH_METHOD_COPY_DIR = "copy_dir";
+ public static final String ATTACH_METHOD_COPY_ID = "copy_id";
+
/** Shared text files are read and their content is stored as note content. */
private static final long MAX_TEXT_FILE_LENGTH_FOR_CONTENT = 1024 * 1024 * 2; // 2 MB
@@ -169,11 +176,13 @@ public Data getDataFromIntent(Intent intent) {
data.bookId = SharingShortcutsManager.bookIdFromShortcutId(shortcutId);
}
- } else if (type.startsWith("image/")) {
- handleSendImage(intent, data); // Handle single image being sent
-
+ } else if (ATTACH_METHOD_COPY_DIR.equals(AppPreferences.attachMethod(this))) {
+ handleCopyFile(intent, data, "file:");
+ } else if (ATTACH_METHOD_COPY_ID.equals(AppPreferences.attachMethod(this))) {
+ handleCopyFile(intent, data, "attachment:");
} else {
- mError = getString(R.string.share_type_not_supported, type);
+ // Link method.
+ handleLinkFile(intent, data);
}
} else if (action.equals("com.google.android.gm.action.AUTO_SEND")) {
@@ -190,6 +199,9 @@ public Data getDataFromIntent(Intent intent) {
data.title = "";
}
+ if (BuildConfig.LOG_DEBUG)
+ LogUtils.d(TAG, "Data title: " + data.title + " attachmentUri: " + data.attachmentUri);
+
return data;
}
@@ -214,7 +226,7 @@ private void setupFragments(Bundle savedInstanceState, Data data) {
}
noteFragment = NoteFragment.forNewNote(
- new NotePlace(bookId), data.title, data.content);
+ new NotePlace(bookId), data.title, data.content, data.attachmentUri);
getSupportFragmentManager()
.beginTransaction()
@@ -310,14 +322,15 @@ public void onError(UseCase action, Throwable throwable) {
private class Data {
String title;
String content;
+ public Uri attachmentUri;
Long bookId = null;
}
/**
- * Get file path from image shared with Orgzly
+ * Get file path shared with Orgzly
* and put it as a file link in the note's content.
*/
- private void handleSendImage(Intent intent, Data data) {
+ private void handleLinkFile(Intent intent, Data data) {
// Get file uri from intent which probably looks like this:
// content://media/external/images/...
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
@@ -344,8 +357,7 @@ private void handleSendImage(Intent intent, Data data) {
if (data.content == null) {
data.content = uri.toString()
- + "\n\nCannot determine path to this image "
- + "and only linking to an image is currently supported.";
+ + "\n\nCannot determine a local path to this file.";
Log.e(TAG, DatabaseUtils.dumpCursorToString(cursor));
}
@@ -362,7 +374,29 @@ private void handleSendImage(Intent intent, Data data) {
if (data.title == null) {
data.title = uri.toString();
- data.content = "Cannot find image using this URI.";
+ data.content = "Cannot find filename using this URI.";
+ }
+ }
+
+ private void handleCopyFile(Intent intent, Data data, String linkPrefix) {
+ Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
+
+ // Get the file name of the content.
+ DocumentFile documentFile = DocumentFile.fromSingleUri(this, uri);
+ String fileName = null;
+ if (documentFile != null) {
+ fileName = documentFile.getName();
+ }
+ if (!OrgStringUtils.isEmpty(fileName)) {
+ data.title = fileName;
+ data.content = "[[" + linkPrefix + fileName + "]]";
+ } else {
+ data.title = uri.toString();
+ data.content = uri.toString() + "\n\nCannot determine fileName to this content.";
}
+
+ // Don't copy the file here, only copy it when a note is saved.
+ // Let's pass the Uri to NoteFragment.
+ data.attachmentUri = uri;
}
}
diff --git a/app/src/main/java/com/orgzly/android/ui/views/richtext/RichText.kt b/app/src/main/java/com/orgzly/android/ui/views/richtext/RichText.kt
index f40ed8a57..ad4693a15 100644
--- a/app/src/main/java/com/orgzly/android/ui/views/richtext/RichText.kt
+++ b/app/src/main/java/com/orgzly/android/ui/views/richtext/RichText.kt
@@ -16,6 +16,7 @@ import android.widget.TextView
import com.orgzly.BuildConfig
import com.orgzly.R
import com.orgzly.android.prefs.AppPreferences
+import com.orgzly.android.ui.AttachmentSpanLoader
import com.orgzly.android.ui.ImageLoader
import com.orgzly.android.ui.main.MainActivity
import com.orgzly.android.ui.util.styledAttributes
@@ -65,6 +66,7 @@ class RichText(context: Context, attrs: AttributeSet?) :
private val richTextEdit: RichTextEdit
private val richTextView: RichTextView
+ var noteId: Long = 0
init {
parseAttrs(attrs)
@@ -182,6 +184,10 @@ class RichText(context: Context, attrs: AttributeSet?) :
richTextView.text = text
}
+ fun getVisibleText(): CharSequence? {
+ return richTextView.text
+ }
+
fun toEditMode(charOffset: Int) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "editable:${attributes.editable}", charOffset)
@@ -209,6 +215,7 @@ class RichText(context: Context, attrs: AttributeSet?) :
richTextView.setText(parsed, TextView.BufferType.SPANNABLE)
+ AttachmentSpanLoader.loadAttachmentPaths(noteId, this)
ImageLoader.loadImages(richTextView)
} else {
diff --git a/app/src/main/java/com/orgzly/android/ui/views/style/AttachmentLinkSpan.kt b/app/src/main/java/com/orgzly/android/ui/views/style/AttachmentLinkSpan.kt
new file mode 100644
index 000000000..acb8abb51
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/ui/views/style/AttachmentLinkSpan.kt
@@ -0,0 +1,30 @@
+package com.orgzly.android.ui.views.style
+
+import android.os.Handler
+import android.text.style.ClickableSpan
+import android.view.View
+import com.orgzly.android.ui.views.richtext.ActionableRichTextView
+
+/**
+ * This [ClickableSpan] corresponds to "attachment:" link. What comes after `:` is `path`. The full
+ * path also needs a prefix which is derived from `ID` property for example.
+ */
+class AttachmentLinkSpan(val path: String) : ClickableSpan() {
+ var prefix: String? = null
+
+ override fun onClick(widget: View) {
+ if (widget is ActionableRichTextView && prefix != null) {
+ Handler().post { // Run after onClick to prevent Snackbar from closing immediately
+ widget.followLinkToFile(getPrefixedPath())
+ }
+ }
+ }
+
+ fun getPrefixedPath(): String {
+ return "$prefix/$path"
+ }
+
+ companion object {
+ const val PREFIX = "attachment:"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/orgzly/android/usecase/FindAttachmentPath.kt b/app/src/main/java/com/orgzly/android/usecase/FindAttachmentPath.kt
new file mode 100644
index 000000000..12af13b9a
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/usecase/FindAttachmentPath.kt
@@ -0,0 +1,40 @@
+package com.orgzly.android.usecase
+
+import android.content.Context
+import com.orgzly.android.App
+import com.orgzly.android.data.DataRepository
+import com.orgzly.android.db.entity.NoteProperty
+import com.orgzly.android.util.AttachmentUtils
+
+/**
+ * An [UseCase] that finds the attachment directory path with the given [noteId].
+ * Corresponds to `org-attach-dir`. Currently checks the ID property of the given node.
+ *
+ * Note that this finds the expected directory path, even if the directory doesn't exist.
+ *
+ * TODO: Also check DIR property.
+ * TODO: Also check inherited property, based on a preference as in `org-attach-use-inheritance`
+ */
+class FindAttachmentPath(val noteId: Long) : UseCase() {
+ val context: Context = App.getAppContext();
+
+ override fun run(dataRepository: DataRepository): UseCaseResult {
+ val noteProperties = dataRepository.getNoteProperties(noteId)
+ val idStr = getProperty(noteProperties, "ID")
+
+ val path = if (idStr == null) null else AttachmentUtils.getAttachDir(context, idStr)
+
+ return UseCaseResult(
+ userData = path
+ )
+ }
+
+ private fun getProperty(noteProperties: List, propertyName: String): String? {
+ for (property: NoteProperty in noteProperties) {
+ if (property.name == propertyName) {
+ return property.value
+ }
+ }
+ return null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/orgzly/android/usecase/NoteCreate.kt b/app/src/main/java/com/orgzly/android/usecase/NoteCreate.kt
index 8dac5a4cc..d6e3b661d 100644
--- a/app/src/main/java/com/orgzly/android/usecase/NoteCreate.kt
+++ b/app/src/main/java/com/orgzly/android/usecase/NoteCreate.kt
@@ -8,6 +8,12 @@ class NoteCreate(val notePayload: NotePayload, val notePlace: NotePlace) : UseCa
override fun run(dataRepository: DataRepository): UseCaseResult {
val note = dataRepository.createNote(notePayload, notePlace)
+ if (notePayload.attachmentUri != null) {
+ dataRepository.storeAttachment(
+ notePlace.bookId,
+ notePayload)
+ }
+
return UseCaseResult(
modifiesLocalData = true,
triggersSync = SYNC_NOTE_CREATED,
diff --git a/app/src/main/java/com/orgzly/android/util/AttachmentUtils.kt b/app/src/main/java/com/orgzly/android/util/AttachmentUtils.kt
new file mode 100644
index 000000000..c8e682dfc
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/util/AttachmentUtils.kt
@@ -0,0 +1,15 @@
+package com.orgzly.android.util
+
+import android.content.Context
+import com.orgzly.android.prefs.AppPreferences
+
+object AttachmentUtils {
+ /** Returns the attachment directory based on ID property. */
+ fun getAttachDir(context: Context, idStr: String) : String {
+ return if (idStr.length <= 2) {
+ AppPreferences.attachDirDefaultPath(context) + "/" + idStr.substring(0, 2)
+ } else {
+ AppPreferences.attachDirDefaultPath(context) + "/" + idStr.substring(0, 2) + "/" + idStr.substring(2)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/orgzly/android/util/OrgFormatter.kt b/app/src/main/java/com/orgzly/android/util/OrgFormatter.kt
index 66cb3ba36..fc96ef2b4 100644
--- a/app/src/main/java/com/orgzly/android/util/OrgFormatter.kt
+++ b/app/src/main/java/com/orgzly/android/util/OrgFormatter.kt
@@ -23,7 +23,7 @@ object OrgFormatter {
private const val SYSTEM_LINK_SCHEMES = "https?|mailto|tel|voicemail|geo|sms|smsto|mms|mmsto"
- private const val CUSTOM_LINK_SCHEMES = "id|file"
+ private const val CUSTOM_LINK_SCHEMES = "id|file|attachment"
// Supported link schemas for plain links
private const val LINK_SCHEMES = "(?:$SYSTEM_LINK_SCHEMES|$CUSTOM_LINK_SCHEMES)"
@@ -191,6 +191,9 @@ object OrgFormatter {
link.startsWith(CustomIdLinkSpan.PREFIX) ->
CustomIdLinkSpan(linkType, link, name)
+ link.startsWith(AttachmentLinkSpan.PREFIX) ->
+ AttachmentLinkSpan(link.substring(11))
+
link.matches("^(?:$SYSTEM_LINK_SCHEMES):.+".toRegex()) ->
UrlLinkSpan(linkType, link, name)
diff --git a/app/src/main/res/values/prefs_keys.xml b/app/src/main/res/values/prefs_keys.xml
index c55b37180..34e555e03 100644
--- a/app/src/main/res/values/prefs_keys.xml
+++ b/app/src/main/res/values/prefs_keys.xml
@@ -462,6 +462,22 @@
pref_key_file_relative_root
+ pref_key_attach_method
+ copy_id
+
+ - @string/attach_method_link
+ - @string/attach_method_copy_dir
+ - @string/attach_method_copy_id
+
+
+ - link
+ - copy_dir
+ - copy_id
+
+
+ pref_key_attach_dir_default_path
+ .
+
pref_key_separate_notes_with_new_line
@string/pref_value_separate_notes_with_new_line_multi_line_notes_only
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8b395b37c..effed02aa 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -707,6 +707,13 @@
Prepend
Insert new note at beginning
+ Method used to add attachments
+ Link
+ Copy to the attachment directory root
+ Copy to ID property based directory under attachment directory
+
+ Attachment directory, a relative path under the repository
+
Developer options
Git repository type
In development
diff --git a/app/src/main/res/xml/prefs_screen_notebooks.xml b/app/src/main/res/xml/prefs_screen_notebooks.xml
index 931bbdd4c..c71787224 100644
--- a/app/src/main/res/xml/prefs_screen_notebooks.xml
+++ b/app/src/main/res/xml/prefs_screen_notebooks.xml
@@ -193,6 +193,23 @@
android:maxLines="1"
app:useSimpleSummaryProvider="true"/>
+
+
+
+