From cb181bdda87511f26e1dca59d4d43e7c5b024f3b Mon Sep 17 00:00:00 2001 From: Xinruo Sun Date: Tue, 22 Dec 2020 21:05:22 -0800 Subject: [PATCH 01/12] Add preferences for attachments. --- .../orgzly/android/prefs/AppPreferences.java | 23 +++++++++++++++++++ app/src/main/res/values/prefs_keys.xml | 16 +++++++++++++ app/src/main/res/values/strings.xml | 7 ++++++ .../main/res/xml/prefs_screen_notebooks.xml | 17 ++++++++++++++ 4 files changed, 63 insertions(+) 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 9236fef97..1eeb0f6d4 100644 --- a/app/src/main/java/com/orgzly/android/prefs/AppPreferences.java +++ b/app/src/main/java/com/orgzly/android/prefs/AppPreferences.java @@ -651,6 +651,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/res/values/prefs_keys.xml b/app/src/main/res/values/prefs_keys.xml index d221a69f9..a38ba2961 100644 --- a/app/src/main/res/values/prefs_keys.xml +++ b/app/src/main/res/values/prefs_keys.xml @@ -445,6 +445,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 3051f51b3..9f8690144 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -699,4 +699,11 @@ 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 diff --git a/app/src/main/res/xml/prefs_screen_notebooks.xml b/app/src/main/res/xml/prefs_screen_notebooks.xml index fdcdba938..92a51671a 100644 --- a/app/src/main/res/xml/prefs_screen_notebooks.xml +++ b/app/src/main/res/xml/prefs_screen_notebooks.xml @@ -203,6 +203,23 @@ android:maxLines="1" app:useSimpleSummaryProvider="true"/> + + + + From ce701ebb14a564dee16784c5b091f6a2b885a883 Mon Sep 17 00:00:00 2001 From: Xinruo Sun Date: Tue, 22 Dec 2020 21:15:12 -0800 Subject: [PATCH 02/12] Accept all file types from sharing, instead of just images. For copy method, pass the content Uri to NoteFragment to be saved. For the link method, keep old behavior. --- .../android/espresso/ShareActivityTest.kt | 15 +++++++ app/src/main/AndroidManifest.xml | 2 +- .../orgzly/android/ui/note/NoteFragment.kt | 4 +- .../android/ui/share/ShareActivity.java | 39 ++++++++++++++++++- 4 files changed, 57 insertions(+), 3 deletions(-) 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 a38f8f883..8a31d55e6 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/ShareActivityTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/espresso/ShareActivityTest.kt @@ -14,6 +14,7 @@ import com.orgzly.R import com.orgzly.android.AppIntent import com.orgzly.android.OrgzlyTest import com.orgzly.android.espresso.EspressoUtils.* +import com.orgzly.android.prefs.AppPreferences import com.orgzly.android.ui.share.ShareActivity import org.hamcrest.Matchers.startsWith import org.junit.Assert.assertTrue @@ -162,6 +163,20 @@ class ShareActivityTest : OrgzlyTest() { onView(withId(R.id.done)).perform(click()) } + @Test + 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.fragment_note_title)).check(matches(withText("content://uri"))) + onView(withId(R.id.body_edit)).check(matches(withText("content://uri\n\nCannot determine fileName to this content."))) + + onView(withId(R.id.done)).perform(click()) + } + @Test fun testNoMatchingType() { startActivityWithIntent(action = Intent.ACTION_SEND, type = "application/octet-stream") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f463ae55f..f8298c8f8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -70,7 +70,7 @@ - + 0) { getInstance( 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 c1e69ab64..e27142c5d 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 @@ -12,6 +12,7 @@ import android.util.Log; import androidx.core.app.TaskStackBuilder; +import androidx.documentfile.provider.DocumentFile; import com.orgzly.BuildConfig; import com.orgzly.R; @@ -21,6 +22,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; @@ -33,6 +35,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; @@ -54,6 +57,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 @@ -160,6 +167,10 @@ public Data getDataFromIntent(Intent intent) { data.bookId = intent.getLongExtra(AppIntent.EXTRA_BOOK_ID, 0L); } + } else if (ATTACH_METHOD_COPY_DIR.equals(AppPreferences.attachMethod(this)) + || ATTACH_METHOD_COPY_ID.equals(AppPreferences.attachMethod(this))) { + handleCopyFile(intent, data); + } else if (type.startsWith("image/")) { handleSendImage(intent, data); // Handle single image being sent @@ -181,6 +192,9 @@ public Data getDataFromIntent(Intent intent) { data.title = ""; } + if (BuildConfig.LOG_DEBUG) + LogUtils.d(TAG, "Data title: " + data.title + " attachmentUri: " + data.attachmentUri); + return data; } @@ -205,7 +219,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() @@ -304,6 +318,7 @@ public void onError(UseCase action, Throwable throwable) { private class Data { String title; String content; + public Uri attachmentUri; Long bookId = null; } @@ -359,4 +374,26 @@ private void handleSendImage(Intent intent, Data data) { data.content = "Cannot find image using this URI."; } } + + private void handleCopyFile(Intent intent, Data data) { + 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 = "attachment:" + 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; + } } From 15f09a260725df70cbeebf5a8aebb067858c0957 Mon Sep 17 00:00:00 2001 From: Xinruo Sun Date: Sat, 28 Nov 2020 23:58:52 -0800 Subject: [PATCH 03/12] Copy and save the attachment in the root of the repo, when creating a note. --- .../com/orgzly/android/data/DataRepository.kt | 40 ++++++++++++++++++- .../com/orgzly/android/ui/note/NoteBuilder.kt | 6 ++- .../orgzly/android/ui/note/NoteFragment.kt | 18 +++++++-- .../com/orgzly/android/ui/note/NotePayload.kt | 24 ++++++++++- .../orgzly/android/ui/note/NoteViewModel.kt | 6 ++- .../android/ui/note/NoteViewModelFactory.kt | 11 +++-- .../com/orgzly/android/usecase/NoteCreate.kt | 6 +++ 7 files changed, 97 insertions(+), 14 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 01f248673..f5870e256 100644 --- a/app/src/main/java/com/orgzly/android/data/DataRepository.kt +++ b/app/src/main/java/com/orgzly/android/data/DataRepository.kt @@ -8,6 +8,7 @@ import android.media.MediaScannerConnection import android.net.Uri import android.os.Handler import android.text.TextUtils +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations @@ -1281,7 +1282,7 @@ class DataRepository @Inject constructor( fun createNoteFromNotification(title: String) { val book = getTargetBook(context) - val notePayload = NoteBuilder.newPayload(context, title, "") + val notePayload = NoteBuilder.newPayload(context, title, "", null) createNote(notePayload, NotePlace(book.book.id)) } @@ -1539,6 +1540,43 @@ class DataRepository @Inject constructor( } } + /** + * Store the attachment content, in the repo for [bookId]. + * + * @throws IOException + */ + @Throws(IOException::class) + fun storeAttachment(bookId: Long, notePayload: NotePayload) { + // Get the fileName from the provider. + // TODO provide a way to customize the fileName + val uri = notePayload.attachmentUri!! + val documentFile: DocumentFile = DocumentFile.fromSingleUri(context, uri) + ?: throw IOException("Cannot get the fileName for Uri $uri") + val fileName = documentFile.name + + val attachDir = notePayload.attachDir(context) + val filePath = attachDir + File.separator + fileName + + val book = getBookView(bookId) + ?: throw IOException(resources.getString(R.string.book_does_not_exist_anymore)) + + // Not quite sure what repo to use. + val repoEntity = book.linkRepo ?: defaultRepoForSavingBook() + val repo = getRepoInstance(repoEntity.id, repoEntity.type, repoEntity.url) + + val tempFile: File + // Get the InputStream of the content and write it to a File. + context.contentResolver.openInputStream(uri).use { inputStream -> + tempFile = getTempBookFile() + MiscUtils.writeStreamToFile(inputStream, tempFile) + LogUtils.d(TAG, "Wrote to file $tempFile") + } + + repo.storeBook(tempFile, filePath) + LogUtils.d(TAG, "Stored file to repo") + tempFile.delete() + } + /** * Loads book from resource. */ 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 194f241f8..0fc0d33a3 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 @@ -67,6 +67,9 @@ class NoteFragment : Fragment(), View.OnClickListener, TimestampDialogFragment.O /** Initial content, used for when sharing. */ private var initialContent: String? = null + /** Content URI of the attachment, used for when a file is being shared. */ + private var attachmentUri: Uri? = null + @Inject internal lateinit var dataRepository: DataRepository @@ -118,6 +121,7 @@ class NoteFragment : Fragment(), View.OnClickListener, TimestampDialogFragment.O initialTitle = getString(ARG_TITLE) initialContent = getString(ARG_CONTENT) + attachmentUri = getString(ARG_ATTACHMENT_URI)?.let { Uri.parse(it) } } } @@ -135,7 +139,8 @@ class NoteFragment : Fragment(), View.OnClickListener, TimestampDialogFragment.O noteId, place, initialTitle, - initialContent) + initialContent, + attachmentUri) viewModel = ViewModelProviders.of(this, factory).get(NoteViewModel::class.java) @@ -1191,6 +1196,7 @@ 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 @@ -1206,7 +1212,8 @@ class NoteFragment : Fragment(), View.OnClickListener, TimestampDialogFragment.O notePlace.noteId, notePlace.place, initialTitle, - initialContent) + initialContent, + attachmentUri) } else { Log.e(TAG, "Invalid book id ${notePlace.bookId}") null @@ -1229,7 +1236,8 @@ class NoteFragment : Fragment(), View.OnClickListener, TimestampDialogFragment.O noteId: Long, place: Place? = null, initialTitle: String? = null, - initialContent: String? = null): NoteFragment { + initialContent: String? = null, + attachmentUri: Uri? = null): NoteFragment { val fragment = NoteFragment() @@ -1253,6 +1261,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..2791bb19d 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,11 @@ 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.org.OrgProperties data class NotePayload @JvmOverloads constructor( @@ -13,7 +17,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 +45,18 @@ data class NotePayload @JvmOverloads constructor( out.writeString(property.value) } } + + out.writeString(attachmentUri.toString()) + } + + fun attachDir(context: Context): String { + // TODO get from property if exists + when(AppPreferences.attachMethod(context)) { + ShareActivity.ATTACH_METHOD_LINK -> return "" + ShareActivity.ATTACH_METHOD_COPY_DIR -> return AppPreferences.attachDirDefaultPath(context) + ShareActivity.ATTACH_METHOD_COPY_ID -> return "" + } + return "" } companion object { @@ -69,6 +86,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 +97,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 5988a20d5..e7ef66570 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 @@ -29,7 +30,8 @@ class NoteViewModel( private var noteId: Long, private val place: Place?, private val title: String?, - private val content: String? + private val content: String?, + private val attachmentUri: Uri? ) : CommonViewModel() { enum class ViewEditMode { @@ -78,7 +80,7 @@ class NoteViewModel( } notePayload = if (isNew()) { - NoteBuilder.newPayload(App.getAppContext(), title ?: "", content) + NoteBuilder.newPayload(App.getAppContext(), title ?: "", content, attachmentUri) } else { dataRepository.getNotePayload(noteId) } diff --git a/app/src/main/java/com/orgzly/android/ui/note/NoteViewModelFactory.kt b/app/src/main/java/com/orgzly/android/ui/note/NoteViewModelFactory.kt index ec7e78a8f..03af2f0cb 100644 --- a/app/src/main/java/com/orgzly/android/ui/note/NoteViewModelFactory.kt +++ b/app/src/main/java/com/orgzly/android/ui/note/NoteViewModelFactory.kt @@ -1,5 +1,6 @@ package com.orgzly.android.ui.note +import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.orgzly.android.data.DataRepository @@ -11,12 +12,13 @@ class NoteViewModelFactory( private val noteId: Long, private val place: Place?, private val title: String?, - private val content: String? + private val content: String?, + private val attachmentUri: Uri? ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @Suppress("UNCHECKED_CAST") - return NoteViewModel(dataRepository, bookId, noteId, place, title, content) as T + return NoteViewModel(dataRepository, bookId, noteId, place, title, content, attachmentUri) as T } companion object { @@ -27,10 +29,11 @@ class NoteViewModelFactory( noteId: Long, place: Place?, title: String?, - content: String? + content: String?, + attachmentUri: Uri? ): ViewModelProvider.Factory { - return NoteViewModelFactory(dataRepository, bookId, noteId, place, title, content) + return NoteViewModelFactory(dataRepository, bookId, noteId, place, title, content, attachmentUri) } } } \ 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, From 38f8a5339434e9c037b087788f3f2b31b7af3bdb Mon Sep 17 00:00:00 2001 From: Xinruo Sun Date: Tue, 22 Dec 2020 21:23:30 -0800 Subject: [PATCH 04/12] Auto generate ID property when creating new note; save attachment to ID based directory. --- .../com/orgzly/android/ui/note/NotePayload.kt | 9 ++++++-- .../orgzly/android/ui/note/NoteViewModel.kt | 21 ++++++++++++++++--- .../orgzly/android/util/AttachmentUtils.kt | 15 +++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/orgzly/android/util/AttachmentUtils.kt 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 2791bb19d..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 @@ -6,6 +6,7 @@ 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( @@ -49,12 +50,16 @@ data class NotePayload @JvmOverloads constructor( out.writeString(attachmentUri.toString()) } + /** Returns the path to store the attachment. */ fun attachDir(context: Context): String { - // TODO get from property if exists + 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 "" + ShareActivity.ATTACH_METHOD_COPY_ID -> { + return AttachmentUtils.getAttachDir(context, idStr) + } } return "" } 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 e7ef66570..b65a4aea2 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 @@ -18,11 +18,13 @@ 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.* class NoteViewModel( private val dataRepository: DataRepository, @@ -79,10 +81,14 @@ class NoteViewModel( dataRepository.getNoteAncestors(noteId) } - notePayload = if (isNew()) { - NoteBuilder.newPayload(App.getAppContext(), title ?: "", content, attachmentUri) + if (isNew()) { + notePayload = NoteBuilder.newPayload(App.getAppContext(), title ?: "", 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 @@ -240,6 +246,15 @@ class NoteViewModel( notePayload = notePayload?.copy(closed = range?.toString()) } + fun updatePayloadCreateIdProperty() { + if (notePayload?.properties!!.containsKey("ID")) { + return + } + notePayload = notePayload?.copy() + val idStr = UUID.randomUUID().toString().toUpperCase() + 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/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 From 5c10f5192aad9f880b359a6240a133d31dcd66bd Mon Sep 17 00:00:00 2001 From: Xinruo Sun Date: Mon, 30 Nov 2020 00:15:09 -0800 Subject: [PATCH 05/12] Add SyncRepo.storeFile. This is to support storing a file into a directory path in the repo. Only implemented for ContentRepo and DirectoryRepo for now. --- .../com/orgzly/android/data/DataRepository.kt | 3 +- .../com/orgzly/android/repos/ContentRepo.java | 32 +++++++++++++++++-- .../orgzly/android/repos/DatabaseRepo.java | 5 +++ .../orgzly/android/repos/DirectoryRepo.java | 5 +++ .../com/orgzly/android/repos/DropboxRepo.java | 5 +++ .../com/orgzly/android/repos/GitRepo.java | 5 +++ .../com/orgzly/android/repos/MockRepo.java | 6 ++++ .../com/orgzly/android/repos/SyncRepo.java | 10 ++++++ .../com/orgzly/android/repos/WebdavRepo.kt | 4 +++ 9 files changed, 71 insertions(+), 4 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 f5870e256..d406ca2c9 100644 --- a/app/src/main/java/com/orgzly/android/data/DataRepository.kt +++ b/app/src/main/java/com/orgzly/android/data/DataRepository.kt @@ -1555,7 +1555,6 @@ class DataRepository @Inject constructor( val fileName = documentFile.name val attachDir = notePayload.attachDir(context) - val filePath = attachDir + File.separator + fileName val book = getBookView(bookId) ?: throw IOException(resources.getString(R.string.book_does_not_exist_anymore)) @@ -1572,7 +1571,7 @@ class DataRepository @Inject constructor( LogUtils.d(TAG, "Wrote to file $tempFile") } - repo.storeBook(tempFile, filePath) + repo.storeFile(tempFile, attachDir, fileName) LogUtils.d(TAG, "Stored file to repo") tempFile.delete() } 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 389e0869e..d16176163 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/DropboxRepo.java b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java index 484da45a6..929903bef 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java @@ -51,6 +51,11 @@ public VersionedRook storeBook(File file, String fileName) throws IOException { return client.upload(file, repoUri, fileName); } + @Override + public VersionedRook storeFile(File file, String pathInRepo, String fileName) throws IOException { + throw new UnsupportedOperationException(); + } + @Override public VersionedRook renameBook(Uri fromUri, String name) throws IOException { Uri toUri = UriUtils.getUriForNewName(fromUri, name); 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 3bb689e98..a68c4ef1a 100644 --- a/app/src/main/java/com/orgzly/android/repos/GitRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/GitRepo.java @@ -193,6 +193,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 d21fd0ff2..59cdae15c 100644 --- a/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt +++ b/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt @@ -164,6 +164,10 @@ class WebdavRepo( return sardine.list(fileUrl).first().toVersionedRook() } + override fun storeFile(file: File?, pathInRepo: String?, fileName: String?): VersionedRook { + TODO("Not yet implemented") + } + override fun renameBook(from: Uri, name: String?): VersionedRook { val destUrl = UriUtils.getUriForNewName(from, name).toUrl() sardine.move(from.toUrl(), destUrl) From a5e6175ce793ecb21d14e252c617aecb981c0d78 Mon Sep 17 00:00:00 2001 From: Xinruo Sun Date: Tue, 22 Dec 2020 20:59:38 -0800 Subject: [PATCH 06/12] Support "attachment:" links. --- .../android/util/OrgFormatterLinkTest.kt | 5 +++ .../orgzly/android/ui/AttachmentSpanLoader.kt | 19 +++++++++ .../java/com/orgzly/android/ui/ImageLoader.kt | 12 ++++-- .../orgzly/android/ui/note/NoteFragment.kt | 2 + .../android/ui/notes/NoteItemViewBinder.kt | 2 + .../ui/views/style/AttachmentLinkSpan.kt | 26 ++++++++++++ .../android/usecase/FindAttachmentPath.kt | 40 +++++++++++++++++++ .../com/orgzly/android/util/OrgFormatter.kt | 5 ++- 8 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/orgzly/android/ui/AttachmentSpanLoader.kt create mode 100644 app/src/main/java/com/orgzly/android/ui/views/style/AttachmentLinkSpan.kt create mode 100644 app/src/main/java/com/orgzly/android/usecase/FindAttachmentPath.kt 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 a98f18ac4..6bd64add6 100644 --- a/app/src/androidTest/java/com/orgzly/android/util/OrgFormatterLinkTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/util/OrgFormatterLinkTest.kt @@ -2,6 +2,7 @@ package com.orgzly.android.util import android.os.Environment 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.IdLinkSpan import org.hamcrest.CoreMatchers.equalTo @@ -54,6 +55,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/java/com/orgzly/android/ui/AttachmentSpanLoader.kt b/app/src/main/java/com/orgzly/android/ui/AttachmentSpanLoader.kt new file mode 100644 index 000000000..fe06accf0 --- /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.TextViewWithMarkup +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, textWithMarkup: TextViewWithMarkup) { + SpanUtils.forEachSpan(textWithMarkup.text 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 c954e32dd..52df6a8c3 100644 --- a/app/src/main/java/com/orgzly/android/ui/ImageLoader.kt +++ b/app/src/main/java/com/orgzly/android/ui/ImageLoader.kt @@ -10,6 +10,7 @@ import android.os.Environment import androidx.core.content.FileProvider import androidx.core.content.res.ResourcesCompat import android.text.Spannable +import android.text.style.ClickableSpan import android.text.style.ImageSpan import android.view.View import com.orgzly.BuildConfig @@ -24,6 +25,7 @@ import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.RequestOptions import com.orgzly.R +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.LogUtils @@ -41,14 +43,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: TextViewWithMarkup, span: FileLinkSpan) { - val path = span.path - + private fun loadImage(textWithMarkup: TextViewWithMarkup, span: 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/NoteFragment.kt b/app/src/main/java/com/orgzly/android/ui/note/NoteFragment.kt index 0fc0d33a3..34bf8d2f3 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 @@ -420,6 +420,7 @@ class NoteFragment : Fragment(), View.OnClickListener, TimestampDialogFragment.O binding.bodyView.setRawText(binding.bodyEdit.text.toString()) + AttachmentSpanLoader.loadAttachmentPaths(noteId, binding.bodyView) ImageLoader.loadImages(binding.bodyView) binding.bodyView.visibility = View.VISIBLE @@ -463,6 +464,7 @@ class NoteFragment : Fragment(), View.OnClickListener, TimestampDialogFragment.O binding.bodyView.setRawText(payload.content ?: "") + AttachmentSpanLoader.loadAttachmentPaths(noteId, binding.bodyView) ImageLoader.loadImages(binding.bodyView) } 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 5c8084704..5eee8eac1 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 @@ -13,6 +13,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.ImageLoader import com.orgzly.android.ui.TimeType import com.orgzly.android.ui.util.TitleGenerator @@ -132,6 +133,7 @@ class NoteItemViewBinder(private val context: Context, private val inBook: Boole } } + AttachmentSpanLoader.loadAttachmentPaths(note.id, holder.binding.itemHeadContent) ImageLoader.loadImages(holder.binding.itemHeadContent) holder.binding.itemHeadContent.visibility = View.VISIBLE 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..c3aab9cd6 --- /dev/null +++ b/app/src/main/java/com/orgzly/android/ui/views/style/AttachmentLinkSpan.kt @@ -0,0 +1,26 @@ +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.TextViewWithMarkup + +/** + * 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 TextViewWithMarkup && prefix != null) { + Handler().post { // Run after onClick to prevent Snackbar from closing immediately + widget.followLinkToFile(getPrefixedPath()) + } + } + } + + fun getPrefixedPath(): String { + return "$prefix/$path" + } +} \ 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/util/OrgFormatter.kt b/app/src/main/java/com/orgzly/android/util/OrgFormatter.kt index e1c841bb0..5c83880e8 100644 --- a/app/src/main/java/com/orgzly/android/util/OrgFormatter.kt +++ b/app/src/main/java/com/orgzly/android/util/OrgFormatter.kt @@ -20,7 +20,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)" @@ -158,6 +158,9 @@ object OrgFormatter { link.startsWith("id:") -> IdLinkSpan(link.substring(3)) + link.startsWith("attachment:") -> + AttachmentLinkSpan(link.substring(11)) + link.startsWith("#") -> CustomIdLinkSpan(link.substring(1)) From 789c8aeb59850e9fc1f2974cc6e20656eb61c7fa Mon Sep 17 00:00:00 2001 From: Xinruo Sun Date: Sun, 14 Feb 2021 18:14:45 -0800 Subject: [PATCH 07/12] Fix link method: allow linking filetypes other than images. --- .../orgzly/android/ui/share/ShareActivity.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) 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 e27142c5d..72790acb4 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 @@ -171,11 +171,9 @@ public Data getDataFromIntent(Intent intent) { || ATTACH_METHOD_COPY_ID.equals(AppPreferences.attachMethod(this))) { handleCopyFile(intent, data); - } else if (type.startsWith("image/")) { - handleSendImage(intent, data); // Handle single image being sent - } 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")) { @@ -323,10 +321,10 @@ private class Data { } /** - * 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); @@ -353,8 +351,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)); } @@ -371,7 +368,7 @@ 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."; } } From 081485a6285f5ba9a552ef38409d7feb41945a20 Mon Sep 17 00:00:00 2001 From: Xinruo Sun Date: Sun, 14 Feb 2021 18:40:30 -0800 Subject: [PATCH 08/12] Fix link created by attach method. --- .../com/orgzly/android/ui/share/ShareActivity.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 72790acb4..7b225ae31 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 @@ -167,10 +167,10 @@ public Data getDataFromIntent(Intent intent) { data.bookId = intent.getLongExtra(AppIntent.EXTRA_BOOK_ID, 0L); } - } else if (ATTACH_METHOD_COPY_DIR.equals(AppPreferences.attachMethod(this)) - || ATTACH_METHOD_COPY_ID.equals(AppPreferences.attachMethod(this))) { - handleCopyFile(intent, data); - + } 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 { // Link method. handleLinkFile(intent, data); @@ -372,7 +372,7 @@ private void handleLinkFile(Intent intent, Data data) { } } - private void handleCopyFile(Intent intent, Data data) { + private void handleCopyFile(Intent intent, Data data, String linkPrefix) { Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); // Get the file name of the content. @@ -383,7 +383,7 @@ private void handleCopyFile(Intent intent, Data data) { } if (!OrgStringUtils.isEmpty(fileName)) { data.title = fileName; - data.content = "attachment:" + fileName; + data.content = "[[" + linkPrefix + fileName + "]]"; } else { data.title = uri.toString(); data.content = uri.toString() + "\n\nCannot determine fileName to this content."; From 8b47540678ffde38699ef4c2dbc8ae9b2650c12b Mon Sep 17 00:00:00 2001 From: Xinruo Sun Date: Mon, 31 May 2021 21:53:02 -0700 Subject: [PATCH 09/12] WebdavRepo: store attachments Tested with a webdav server to confirm that attachment and its parent directories can be created. Caveat: - The attachment: link cannot be resolved. Because this link currently only resolves to local files, it cannot deal with remote files (like webdav). To handle remote files, need additional changes to download and cache the remote files. --- .../com/orgzly/android/repos/WebdavRepo.kt | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) 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 59cdae15c..dfc11b021 100644 --- a/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt +++ b/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt @@ -8,6 +8,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 @@ -165,7 +166,36 @@ class WebdavRepo( } override fun storeFile(file: File?, pathInRepo: String?, fileName: String?): VersionedRook { - TODO("Not yet implemented") + 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 { From c2a2ab415849a73f61ddb5bc2ad364edf09f523a Mon Sep 17 00:00:00 2001 From: Xinruo Sun Date: Sun, 16 Jan 2022 08:38:19 -0800 Subject: [PATCH 10/12] Rename .java to .kt --- .../com/orgzly/android/repos/{DropboxRepo.java => DropboxRepo.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/orgzly/android/repos/{DropboxRepo.java => DropboxRepo.kt} (100%) diff --git a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.kt similarity index 100% rename from app/src/main/java/com/orgzly/android/repos/DropboxRepo.java rename to app/src/main/java/com/orgzly/android/repos/DropboxRepo.kt From 142ec6d141c22a5fc40dbdaab7a9376271285dd4 Mon Sep 17 00:00:00 2001 From: Xinruo Sun Date: Sun, 16 Jan 2022 08:38:20 -0800 Subject: [PATCH 11/12] Convert DropboxRepo to Kotlin. --- .../orgzly/android/repos/DropboxClient.java | 9 +- .../com/orgzly/android/repos/DropboxRepo.kt | 99 +++++++++---------- 2 files changed, 53 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/com/orgzly/android/repos/DropboxClient.java b/app/src/main/java/com/orgzly/android/repos/DropboxClient.java index 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.kt b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.kt index 929903bef..af8d28acd 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.kt +++ b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.kt @@ -1,74 +1,69 @@ -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; +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.IOException +import java.lang.UnsupportedOperationException + +class DropboxRepo(repoWithProps: RepoWithProps, context: Context?) : SyncRepo { + private val repoUri: Uri + private val client: DropboxClient + override fun isConnectionRequired(): Boolean { + return true + } - public DropboxRepo(RepoWithProps repoWithProps, Context context) { - this.repoUri = Uri.parse(repoWithProps.getRepo().getUrl()); - this.client = new DropboxClient(context, repoWithProps.getRepo().getId()); + override fun isAutoSyncSupported(): Boolean { + return false } - @Override - public boolean isConnectionRequired() { - return true; + override fun getUri(): Uri { + return repoUri } - @Override - public boolean isAutoSyncSupported() { - return false; + @Throws(IOException::class) + override fun getBooks(): List { + return client.getBooks(repoUri) } - @Override - public Uri getUri() { - return repoUri; + @Throws(IOException::class) + override fun retrieveBook(fileName: String, file: File): VersionedRook { + return client.download(repoUri, fileName, file) } - @Override - public List getBooks() throws IOException { - return client.getBooks(repoUri); + @Throws(IOException::class) + override fun storeBook(file: File, fileName: String): VersionedRook { + return client.upload(file, repoUri, fileName) } - @Override - public VersionedRook retrieveBook(String fileName, File file) throws IOException { - return client.download(repoUri, fileName, file); + @Throws(IOException::class) + override fun storeFile(file: File, pathInRepo: String, fileName: String): VersionedRook { + throw UnsupportedOperationException() } - @Override - public VersionedRook storeBook(File file, String fileName) throws IOException { - return client.upload(file, repoUri, fileName); + @Throws(IOException::class) + override fun renameBook(fromUri: Uri, name: String): VersionedRook { + val toUri = UriUtils.getUriForNewName(fromUri, name) + return client.move(repoUri, fromUri, toUri) } - @Override - public VersionedRook storeFile(File file, String pathInRepo, String fileName) throws IOException { - throw new UnsupportedOperationException(); + @Throws(IOException::class) + override fun delete(uri: Uri) { + client.delete(uri.path) } - @Override - public VersionedRook renameBook(Uri fromUri, String name) throws IOException { - Uri toUri = UriUtils.getUriForNewName(fromUri, name); - return client.move(repoUri, fromUri, toUri); + override fun toString(): String { + return repoUri.toString() } - @Override - public void delete(Uri uri) throws IOException { - client.delete(uri.getPath()); + companion object { + const val SCHEME = "dropbox" } - @Override - public String toString() { - return repoUri.toString(); + init { + repoUri = Uri.parse(repoWithProps.repo.url) + client = DropboxClient(context, repoWithProps.repo.id) } -} +} \ No newline at end of file From 1c368f64392b3e86e51ca6826e5f8283059bd805 Mon Sep 17 00:00:00 2001 From: Xinruo Sun Date: Sun, 16 Jan 2022 08:45:27 -0800 Subject: [PATCH 12/12] DropboxRepo: store attachments Tested with Dropbox to confirm that attachment and its parent directories can be created. --- .../main/java/com/orgzly/android/repos/DropboxRepo.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.kt b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.kt index af8d28acd..6184e20d2 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.kt +++ b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.kt @@ -5,8 +5,8 @@ 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 -import java.lang.UnsupportedOperationException class DropboxRepo(repoWithProps: RepoWithProps, context: Context?) : SyncRepo { private val repoUri: Uri @@ -40,7 +40,12 @@ class DropboxRepo(repoWithProps: RepoWithProps, context: Context?) : SyncRepo { @Throws(IOException::class) override fun storeFile(file: File, pathInRepo: String, fileName: String): VersionedRook { - throw UnsupportedOperationException() + 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)