Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for attachments #130

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

apply plugin: 'kotlin-parcelize'

android {
namespace 'com.orgzly'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
package com.orgzly.android.espresso

import android.app.Activity
import android.app.Instrumentation
import android.content.Intent
import android.content.pm.ActivityInfo
import android.widget.DatePicker
import android.widget.TimePicker
import androidx.documentfile.provider.DocumentFile
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.*
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.PickerActions.setDate
import androidx.test.espresso.contrib.PickerActions.setTime
import androidx.test.espresso.core.internal.deps.guava.collect.Iterables
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers.anyIntent
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.*
import com.orgzly.R
import com.orgzly.android.OrgzlyTest
import com.orgzly.android.espresso.util.EspressoUtils.*
import com.orgzly.android.prefs.AppPreferences
import com.orgzly.android.repos.RepoType
import com.orgzly.android.ui.main.MainActivity
import com.orgzly.android.ui.share.ShareActivity
import com.orgzly.android.util.MiscUtils
import org.hamcrest.Matchers.*
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import java.io.File

class NoteFragmentTest : OrgzlyTest() {
private lateinit var scenario: ActivityScenario<MainActivity>
Expand Down Expand Up @@ -545,4 +559,89 @@ class NoteFragmentTest : OrgzlyTest() {

onView(withId(R.id.fragment_book_view_flipper)).check(matches(isDisplayed()))
}

@Test
fun testAttachmentsHiddenByDefault() {
onNoteInBook(1).perform(click())
// By default attachments list is hidden.
onView(withId(R.id.attachments_header_up_icon)).check(matches(not(isDisplayed())))
onView(withId(R.id.attachments_header_down_icon)).check(matches(not(isDisplayed())))

// The attachments header is hidden.
onView(withId(R.id.attachments_header)).check(matches(not(isDisplayed())))
onView(withId(R.id.attachments_header_add_button)).check(matches(not(isDisplayed())))
}

@Test
fun testAttachmentsAddFirst() {
onNoteInBook(1).perform(click())

// Stub response to pick a file.
Intents.init()
stubFilePicker()

// Click attach file in the menu.
openActionBarOverflowOrOptionsMenu(context)
onView(withText(R.string.attachment_add)).perform(click())

// Verify the intent was sent correctly.
val receivedIntent = Iterables.getOnlyElement(Intents.getIntents())
Assert.assertThat(receivedIntent, hasAction(Intent.ACTION_CHOOSER))
val extraIntent : Intent = receivedIntent.extras?.get(Intent.EXTRA_INTENT) as Intent
Assert.assertThat(extraIntent, hasAction(Intent.ACTION_GET_CONTENT))

// Verify the list.
onView(withId(R.id.attachments_header)).check(matches(isDisplayed()))
onView(withId(R.id.attachments_header_up_icon)).check(matches(isDisplayed()))
onView(withId(R.id.attachments_header_down_icon)).check(matches(not(isDisplayed())))
onView(withText(ATTACHMENT_FILE_NAME)).check(matches(isDisplayed()))
}

@Test
fun testNewNoteAddAttachment_autoAddIdProperty() {
AppPreferences.attachMethod(context, ShareActivity.ATTACH_METHOD_COPY_ID);
val dataDir = File(context.cacheDir, "data")
testUtils.setupRepo(RepoType.DIRECTORY, dataDir.toURI().toString())

onNoteInBook(1).perform(longClick())
onActionItemClick(R.id.new_note, R.string.new_note)
onView(withText(R.string.new_above)).perform(click())

onView(withId(R.id.title_edit)).perform(*replaceTextCloseKeyboard("Note with attachment"))

// Stub response to pick a file.
Intents.init()
stubFilePicker()

// Click attach file in the menu.
openActionBarOverflowOrOptionsMenu(context)
onView(withText(R.string.attachment_add)).perform(click())

// Verify ID property is set.
onView(allOf(withId(R.id.name), withText("ID"))).check(matches(isDisplayed()))

// Save and reopen the note.
onView(withId(R.id.done)).perform(click())
onNoteInBook(1).perform(click())

// Verify ID property is saved.
onView(allOf(withId(R.id.name), withText("ID"))).check(matches(isDisplayed()))
// Verify the attachment list is shown
onView(withId(R.id.attachments_header_up_icon)).check(matches(isDisplayed()))
onView(withId(R.id.attachments_header_down_icon)).check(matches(not(isDisplayed())))
onView(withText(EXPECTED_ATTACHMENT_FILE_NAME)).check(matches(isDisplayed()))
}

private val ATTACHMENT_FILE_NAME = "cat.jpg"
// Filename cannot be mocked in DocumentFile easily, it is "null" during storeFile.
private val EXPECTED_ATTACHMENT_FILE_NAME = "null"

private fun stubFilePicker() {
val resultData = Intent()
val file = File(context.cacheDir, ATTACHMENT_FILE_NAME)
MiscUtils.writeStringToFile("cat image", file)
resultData.data = DocumentFile.fromFile(file).uri
val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData)
Intents.intending(anyIntent()).respondWith(result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ package com.orgzly.android.espresso
import android.content.Intent
import android.content.pm.ActivityInfo
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.matcher.ViewMatchers.*
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
import org.junit.Test
import java.io.File


class ShareActivityTest : OrgzlyTest() {
Expand Down Expand Up @@ -156,17 +158,52 @@ 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_fillBody() {
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
fun testFileCopy_attachmentsList() {
AppPreferences.attachMethod(context, ShareActivity.ATTACH_METHOD_COPY_DIR);

val file = File(context.cacheDir, "test.pdf")
val uri = DocumentFile.fromFile(file).uri

val scenario = startActivityWithIntent(
action = Intent.ACTION_SEND,
type = "application/pdf",
extraStreamUri = uri.toString())

// Check if the file is displayed in the list in portrait mode.
scenario.onActivity { activity ->
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
onView(withText("test.pdf")).check(matches(isDisplayed()));

// Check in landscape mode, scroll down to right at the body view to make sure we are at the
// end of the attachment list.
scenario.onActivity { activity ->
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
onView(withId(R.id.content_view)).perform(scroll())
onView(withText("test.pdf")).check(matches(isDisplayed()))
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.orgzly.android.util

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
Expand Down Expand Up @@ -67,6 +68,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))),
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="*/*" />
</intent-filter>

<meta-data
Expand Down
48 changes: 48 additions & 0 deletions app/src/main/java/com/orgzly/android/data/DataRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -36,6 +37,7 @@ import com.orgzly.android.savedsearch.FileSavedSearchStore
import com.orgzly.android.sync.BookSyncStatus
import com.orgzly.android.ui.NotePlace
import com.orgzly.android.ui.Place
import com.orgzly.android.ui.note.NoteAttachmentData
import com.orgzly.android.ui.note.NoteBuilder
import com.orgzly.android.ui.note.NotePayload
import com.orgzly.android.usecase.RepoCreate
Expand Down Expand Up @@ -1597,6 +1599,52 @@ class DataRepository @Inject constructor(
}
}

/**
* Store the attachment content, in the repo for [bookId].
*
* @throws IOException
*/
@Throws(IOException::class)
fun storeAttachment(bookId: Long, notePayload: NotePayload, attachmentUri: Uri) {
// Get the fileName from the provider.
// TODO provide a way to customize the fileName
val uri = 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 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.storeFile(tempFile, attachDir, fileName)
LogUtils.d(TAG, "Stored file to repo")
tempFile.delete()
}

fun listFiles(bookId: Long, notePayload: NotePayload): List<NoteAttachmentData> {
val book = getBookView(bookId)
?: throw IOException(resources.getString(R.string.book_does_not_exist_anymore))
val repoEntity = book.linkRepo ?: defaultRepoForSavingBook()
val repo = getRepoInstance(repoEntity.id, repoEntity.type, repoEntity.url)
val attachDir = notePayload.attachDir(context)

return repo.listFilesInPath(attachDir)
}

/**
* Loads book from resource.
*/
Expand Down
23 changes: 23 additions & 0 deletions app/src/main/java/com/orgzly/android/prefs/AppPreferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,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
*/
Expand Down
Loading
Loading