Skip to content

Commit e667da9

Browse files
committed
Support "attachment:" links.
1 parent 1b6b848 commit e667da9

File tree

8 files changed

+106
-5
lines changed

8 files changed

+106
-5
lines changed

app/src/androidTest/java/com/orgzly/android/util/OrgFormatterLinkTest.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.orgzly.android.util
22

33
import android.os.Environment
44
import android.text.style.URLSpan
5+
import com.orgzly.android.ui.views.style.AttachmentLinkSpan
56
import com.orgzly.android.ui.views.style.FileLinkSpan
67
import com.orgzly.android.ui.views.style.IdLinkSpan
78
import org.hamcrest.CoreMatchers.equalTo
@@ -54,6 +55,10 @@ class OrgFormatterLinkTest(private val param: Parameter) : OrgFormatterTest() {
5455
Parameter("[[file:orgzly-tests/document.txt]]", "file:orgzly-tests/document.txt", listOf(Span(0, 30, FileLinkSpan::class.java))),
5556
Parameter("[[file:orgzly-tests/document.txt][Document]]", "Document", listOf(Span(0, 8, FileLinkSpan::class.java))),
5657

58+
Parameter("attachment:orgzly-tests/document.txt", "attachment:orgzly-tests/document.txt", listOf(Span(0, 36, AttachmentLinkSpan::class.java))),
59+
Parameter("[[attachment:orgzly-tests/document.txt]]", "attachment:orgzly-tests/document.txt", listOf(Span(0, 36, AttachmentLinkSpan::class.java))),
60+
Parameter("[[attachment:orgzly-tests/document.txt][Document]]", "Document", listOf(Span(0, 8, AttachmentLinkSpan::class.java))),
61+
5762
Parameter("id:45DFE015-255E-4B86-B957-F7FD77364DCA", "id:45DFE015-255E-4B86-B957-F7FD77364DCA", listOf(Span(0, 39, IdLinkSpan::class.java))),
5863
Parameter("[[id:45DFE015-255E-4B86-B957-F7FD77364DCA]]", "id:45DFE015-255E-4B86-B957-F7FD77364DCA", listOf(Span(0, 39, IdLinkSpan::class.java))),
5964
Parameter("id:foo", "id:foo", listOf(Span(0, 6, IdLinkSpan::class.java))),
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.orgzly.android.ui
2+
3+
import android.text.Spannable
4+
import com.orgzly.android.ui.views.TextViewWithMarkup
5+
import com.orgzly.android.ui.views.style.AttachmentLinkSpan
6+
import com.orgzly.android.usecase.FindAttachmentPath
7+
import com.orgzly.android.usecase.UseCaseRunner
8+
9+
object AttachmentSpanLoader {
10+
/** Find all `attachment:` links and set up the prefix directory based on `ID` property. */
11+
fun loadAttachmentPaths(noteId: Long, textWithMarkup: TextViewWithMarkup) {
12+
SpanUtils.forEachSpan(textWithMarkup.text as Spannable, AttachmentLinkSpan::class.java) { span ->
13+
val prefix = UseCaseRunner.run(FindAttachmentPath(noteId)).userData
14+
if (prefix != null) {
15+
span.prefix = prefix as String
16+
}
17+
}
18+
}
19+
}

app/src/main/java/com/orgzly/android/ui/ImageLoader.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import android.os.Environment
1010
import androidx.core.content.FileProvider
1111
import androidx.core.content.res.ResourcesCompat
1212
import android.text.Spannable
13+
import android.text.style.ClickableSpan
1314
import android.text.style.ImageSpan
1415
import android.view.View
1516
import com.orgzly.BuildConfig
@@ -24,6 +25,7 @@ import com.bumptech.glide.request.target.SimpleTarget
2425
import com.bumptech.glide.request.transition.Transition
2526
import com.bumptech.glide.request.RequestOptions
2627
import com.orgzly.R
28+
import com.orgzly.android.ui.views.style.AttachmentLinkSpan
2729
import com.orgzly.android.usecase.LinkFindTarget
2830
import com.orgzly.android.usecase.UseCaseRunner
2931
import com.orgzly.android.util.LogUtils
@@ -41,14 +43,16 @@ object ImageLoader {
4143
&& AppPermissions.isGranted(context, AppPermissions.Usage.EXTERNAL_FILES_ACCESS)) {
4244
// Load the associated image for each FileLinkSpan
4345
SpanUtils.forEachSpan(textWithMarkup.text as Spannable, FileLinkSpan::class.java) { span ->
44-
loadImage(textWithMarkup, span)
46+
loadImage(textWithMarkup, span, span.path)
47+
}
48+
// Load the associated image for each AttachmentLinkSpan
49+
SpanUtils.forEachSpan(textWithMarkup.text as Spannable, AttachmentLinkSpan::class.java) { span ->
50+
loadImage(textWithMarkup, span, span.getPrefixedPath())
4551
}
4652
}
4753
}
4854

49-
private fun loadImage(textWithMarkup: TextViewWithMarkup, span: FileLinkSpan) {
50-
val path = span.path
51-
55+
private fun loadImage(textWithMarkup: TextViewWithMarkup, span: ClickableSpan, path: String) {
5256
if (hasSupportedExtension(path)) {
5357
val text = textWithMarkup.text as Spannable
5458
// Get the current context

app/src/main/java/com/orgzly/android/ui/note/NoteFragment.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ class NoteFragment : Fragment(), View.OnClickListener, TimestampDialogFragment.O
420420

421421
binding.bodyView.setRawText(binding.bodyEdit.text.toString())
422422

423+
AttachmentSpanLoader.loadAttachmentPaths(noteId, binding.bodyView)
423424
ImageLoader.loadImages(binding.bodyView)
424425

425426
binding.bodyView.visibility = View.VISIBLE
@@ -463,6 +464,7 @@ class NoteFragment : Fragment(), View.OnClickListener, TimestampDialogFragment.O
463464

464465
binding.bodyView.setRawText(payload.content ?: "")
465466

467+
AttachmentSpanLoader.loadAttachmentPaths(noteId, binding.bodyView)
466468
ImageLoader.loadImages(binding.bodyView)
467469
}
468470

app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.orgzly.android.App
1313
import com.orgzly.android.db.entity.Note
1414
import com.orgzly.android.db.entity.NoteView
1515
import com.orgzly.android.prefs.AppPreferences
16+
import com.orgzly.android.ui.AttachmentSpanLoader
1617
import com.orgzly.android.ui.ImageLoader
1718
import com.orgzly.android.ui.TimeType
1819
import com.orgzly.android.ui.util.TitleGenerator
@@ -132,6 +133,7 @@ class NoteItemViewBinder(private val context: Context, private val inBook: Boole
132133
}
133134
}
134135

136+
AttachmentSpanLoader.loadAttachmentPaths(note.id, holder.binding.itemHeadContent)
135137
ImageLoader.loadImages(holder.binding.itemHeadContent)
136138

137139
holder.binding.itemHeadContent.visibility = View.VISIBLE
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.orgzly.android.ui.views.style
2+
3+
import android.os.Handler
4+
import android.text.style.ClickableSpan
5+
import android.view.View
6+
import com.orgzly.android.ui.views.TextViewWithMarkup
7+
8+
/**
9+
* This [ClickableSpan] corresponds to "attachment:" link. What comes after `:` is `path`. The full
10+
* path also needs a prefix which is derived from `ID` property for example.
11+
*/
12+
class AttachmentLinkSpan(val path: String) : ClickableSpan() {
13+
var prefix: String? = null
14+
15+
override fun onClick(widget: View) {
16+
if (widget is TextViewWithMarkup && prefix != null) {
17+
Handler().post { // Run after onClick to prevent Snackbar from closing immediately
18+
widget.followLinkToFile(getPrefixedPath())
19+
}
20+
}
21+
}
22+
23+
fun getPrefixedPath(): String {
24+
return "$prefix/$path"
25+
}
26+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.orgzly.android.usecase
2+
3+
import android.content.Context
4+
import com.orgzly.android.App
5+
import com.orgzly.android.data.DataRepository
6+
import com.orgzly.android.db.entity.NoteProperty
7+
import com.orgzly.android.util.AttachmentUtils
8+
9+
/**
10+
* An [UseCase] that finds the attachment directory path with the given [noteId].
11+
* Corresponds to `org-attach-dir`. Currently checks the ID property of the given node.
12+
*
13+
* Note that this finds the expected directory path, even if the directory doesn't exist.
14+
*
15+
* TODO: Also check DIR property.
16+
* TODO: Also check inherited property, based on a preference as in `org-attach-use-inheritance`
17+
*/
18+
class FindAttachmentPath(val noteId: Long) : UseCase() {
19+
val context: Context = App.getAppContext();
20+
21+
override fun run(dataRepository: DataRepository): UseCaseResult {
22+
val noteProperties = dataRepository.getNoteProperties(noteId)
23+
val idStr = getProperty(noteProperties, "ID")
24+
25+
val path = if (idStr == null) null else AttachmentUtils.getAttachDir(context, idStr)
26+
27+
return UseCaseResult(
28+
userData = path
29+
)
30+
}
31+
32+
private fun getProperty(noteProperties: List<NoteProperty>, propertyName: String): String? {
33+
for (property: NoteProperty in noteProperties) {
34+
if (property.name == propertyName) {
35+
return property.value
36+
}
37+
}
38+
return null
39+
}
40+
}

app/src/main/java/com/orgzly/android/util/OrgFormatter.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ object OrgFormatter {
2020

2121
private const val SYSTEM_LINK_SCHEMES = "https?|mailto|tel|voicemail|geo|sms|smsto|mms|mmsto"
2222

23-
private const val CUSTOM_LINK_SCHEMES = "id|file"
23+
private const val CUSTOM_LINK_SCHEMES = "id|file|attachment"
2424

2525
// Supported link schemas for plain links
2626
private const val LINK_SCHEMES = "(?:$SYSTEM_LINK_SCHEMES|$CUSTOM_LINK_SCHEMES)"
@@ -158,6 +158,9 @@ object OrgFormatter {
158158
link.startsWith("id:") ->
159159
IdLinkSpan(link.substring(3))
160160

161+
link.startsWith("attachment:") ->
162+
AttachmentLinkSpan(link.substring(11))
163+
161164
link.startsWith("#") ->
162165
CustomIdLinkSpan(link.substring(1))
163166

0 commit comments

Comments
 (0)