mirror of
https://github.com/nextcloud/android.git
synced 2024-11-28 18:28:59 +03:00
Merge pull request #6806 from nextcloud/grote/improve-documentsprovider-add-tests
Improve DocumentsProvider and add tests
This commit is contained in:
commit
f807397bdd
6 changed files with 501 additions and 113 deletions
|
@ -1 +1 @@
|
||||||
329
|
326
|
|
@ -0,0 +1,198 @@
|
||||||
|
package com.owncloud.android.providers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.ContentObserver
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID
|
||||||
|
import android.provider.DocumentsContract.Document.COLUMN_MIME_TYPE
|
||||||
|
import android.provider.DocumentsContract.Document.MIME_TYPE_DIR
|
||||||
|
import android.provider.DocumentsContract.EXTRA_LOADING
|
||||||
|
import android.provider.DocumentsContract.buildChildDocumentsUriUsingTree
|
||||||
|
import android.provider.DocumentsContract.buildDocumentUriUsingTree
|
||||||
|
import android.provider.DocumentsContract.buildTreeDocumentUri
|
||||||
|
import android.provider.DocumentsContract.getDocumentId
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||||
|
import com.owncloud.android.datamodel.OCFile
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation
|
||||||
|
import com.owncloud.android.providers.DocumentsStorageProvider.DOCUMENTID_SEPARATOR
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
// Uploads can sometimes take a bit of time, so 15sec is still considered recent enough
|
||||||
|
private const val RECENT_MILLISECONDS = 15_000
|
||||||
|
|
||||||
|
object DocumentsProviderUtils {
|
||||||
|
|
||||||
|
internal fun DocumentFile.getOCFile(storageManager: FileDataStorageManager): OCFile? {
|
||||||
|
val id = getDocumentId(uri)
|
||||||
|
val separated: List<String> = id.split(DOCUMENTID_SEPARATOR.toRegex())
|
||||||
|
return storageManager.getFileById(separated[1].toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun DocumentFile.assertRegularFile(
|
||||||
|
name: String? = null,
|
||||||
|
size: Long? = null,
|
||||||
|
mimeType: String? = null,
|
||||||
|
parent: DocumentFile? = null
|
||||||
|
) {
|
||||||
|
name?.let { assertEquals(it, this.name) }
|
||||||
|
assertTrue(exists())
|
||||||
|
assertTrue(isFile)
|
||||||
|
assertFalse(isDirectory)
|
||||||
|
assertFalse(isVirtual)
|
||||||
|
size?.let { assertEquals(it, length()) }
|
||||||
|
mimeType?.let { assertEquals(it, type) }
|
||||||
|
parent?.let { assertEquals(it.uri.toString(), parentFile!!.uri.toString()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun DocumentFile.assertRegularFolder(name: String? = null, parent: DocumentFile? = null) {
|
||||||
|
name?.let { assertEquals(it, this.name) }
|
||||||
|
assertTrue(exists())
|
||||||
|
assertFalse(isFile)
|
||||||
|
assertTrue(isDirectory)
|
||||||
|
assertFalse(isVirtual)
|
||||||
|
parent?.let { assertEquals(it.uri.toString(), parentFile!!.uri.toString()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun DocumentFile.assertRecentlyModified() {
|
||||||
|
val diff = System.currentTimeMillis() - lastModified()
|
||||||
|
assertTrue("File $name older than expected: $diff", diff < RECENT_MILLISECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun assertExistsOnServer(client: OwnCloudClient, remotePath: String, shouldExit: Boolean) {
|
||||||
|
val result = ExistenceCheckRemoteOperation(remotePath, !shouldExit).execute(client)
|
||||||
|
assertTrue("$result", result.isSuccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun assertListFilesEquals(expected: Collection<DocumentFile>, actual: Collection<DocumentFile>) {
|
||||||
|
// assertEquals(
|
||||||
|
// "Actual: ${actual.map { it.name.toString() }}",
|
||||||
|
// expected.map { it.uri.toString() }.apply { sorted() },
|
||||||
|
// actual.map { it.uri.toString() }.apply { sorted() },
|
||||||
|
// )
|
||||||
|
// FIXME replace with commented out stronger assertion above
|
||||||
|
// when parallel [UploadFileOperation]s don't bring back deleted files
|
||||||
|
val expectedSet = HashSet<String>(expected.map { it.uri.toString() })
|
||||||
|
val actualSet = HashSet<String>(actual.map { it.uri.toString() })
|
||||||
|
assertTrue(actualSet.containsAll(expectedSet))
|
||||||
|
actualSet.removeAll(expectedSet)
|
||||||
|
actualSet.forEach {
|
||||||
|
Log.e("TEST", "Error: Found unexpected file: $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun assertReadEquals(data: ByteArray, inputStream: InputStream?) {
|
||||||
|
assertNotNull(inputStream)
|
||||||
|
inputStream!!.use {
|
||||||
|
assertArrayEquals(data, it.readBytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as [DocumentFile.findFile] only that it re-queries when the first result was stale.
|
||||||
|
*
|
||||||
|
* Most documents providers including Nextcloud are listing the full directory content
|
||||||
|
* when querying for a specific file in a directory,
|
||||||
|
* so there is no point in trying to optimize the query by not listing all children.
|
||||||
|
*/
|
||||||
|
suspend fun DocumentFile.findFileBlocking(context: Context, displayName: String): DocumentFile? {
|
||||||
|
val files = listFilesBlocking(context)
|
||||||
|
for (doc in files) {
|
||||||
|
if (displayName == doc.name) return doc
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Works like [DocumentFile.listFiles] except that it waits until the DocumentProvider has a result.
|
||||||
|
* This prevents getting an empty list even though there are children to be listed.
|
||||||
|
*/
|
||||||
|
suspend fun DocumentFile.listFilesBlocking(context: Context) = withContext(Dispatchers.IO) {
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
val childrenUri = buildChildDocumentsUriUsingTree(uri, getDocumentId(uri))
|
||||||
|
val projection = arrayOf(COLUMN_DOCUMENT_ID, COLUMN_MIME_TYPE)
|
||||||
|
val result = ArrayList<DocumentFile>()
|
||||||
|
|
||||||
|
try {
|
||||||
|
getLoadedCursor {
|
||||||
|
resolver.query(childrenUri, projection, null, null, null)
|
||||||
|
}
|
||||||
|
} catch (e: TimeoutCancellationException) {
|
||||||
|
throw IOException(e)
|
||||||
|
}.use { cursor ->
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val documentId = cursor.getString(0)
|
||||||
|
val isDirectory = cursor.getString(1) == MIME_TYPE_DIR
|
||||||
|
val file = if (isDirectory) {
|
||||||
|
val treeUri = buildTreeDocumentUri(uri.authority, documentId)
|
||||||
|
DocumentFile.fromTreeUri(context, treeUri)!!
|
||||||
|
} else {
|
||||||
|
val documentUri = buildDocumentUriUsingTree(uri, documentId)
|
||||||
|
DocumentFile.fromSingleUri(context, documentUri)!!
|
||||||
|
}
|
||||||
|
result.add(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a cursor for the given query while ensuring that the cursor was loaded.
|
||||||
|
*
|
||||||
|
* When the SAF backend is a cloud storage provider (e.g. Nextcloud),
|
||||||
|
* it can happen that the query returns an outdated (e.g. empty) cursor
|
||||||
|
* which will only be updated in response to this query.
|
||||||
|
*
|
||||||
|
* See: https://commonsware.com/blog/2019/12/14/scoped-storage-stories-listfiles-woe.html
|
||||||
|
*
|
||||||
|
* This method uses a [suspendCancellableCoroutine] to wait for the result of a [ContentObserver]
|
||||||
|
* registered on the cursor in case the cursor is still loading ([EXTRA_LOADING]).
|
||||||
|
* If the cursor is not loading, it will be returned right away.
|
||||||
|
*
|
||||||
|
* @param timeout an optional time-out in milliseconds
|
||||||
|
* @throws TimeoutCancellationException if there was no result before the time-out
|
||||||
|
* @throws IOException if the query returns null
|
||||||
|
*/
|
||||||
|
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||||
|
@VisibleForTesting
|
||||||
|
internal suspend fun getLoadedCursor(timeout: Long = 15_000, query: () -> Cursor?) =
|
||||||
|
withTimeout(timeout) {
|
||||||
|
suspendCancellableCoroutine<Cursor> { cont ->
|
||||||
|
val cursor = query() ?: throw IOException("Initial query returned no results")
|
||||||
|
cont.invokeOnCancellation { cursor.close() }
|
||||||
|
val loading = cursor.extras.getBoolean(EXTRA_LOADING, false)
|
||||||
|
if (loading) {
|
||||||
|
Log.e("TEST", "Cursor was loading, wait for update...")
|
||||||
|
cursor.registerContentObserver(
|
||||||
|
object : ContentObserver(null) {
|
||||||
|
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
||||||
|
cursor.close()
|
||||||
|
val newCursor = query()
|
||||||
|
if (newCursor == null) cont.cancel(IOException("Re-query returned no results"))
|
||||||
|
else cont.resume(newCursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// not loading, return cursor right away
|
||||||
|
cont.resume(cursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
package com.owncloud.android.providers
|
||||||
|
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.owncloud.android.AbstractOnServerIT
|
||||||
|
import com.owncloud.android.R
|
||||||
|
import com.owncloud.android.datamodel.OCFile.ROOT_PATH
|
||||||
|
import com.owncloud.android.providers.DocumentsProviderUtils.assertExistsOnServer
|
||||||
|
import com.owncloud.android.providers.DocumentsProviderUtils.assertListFilesEquals
|
||||||
|
import com.owncloud.android.providers.DocumentsProviderUtils.assertReadEquals
|
||||||
|
import com.owncloud.android.providers.DocumentsProviderUtils.assertRecentlyModified
|
||||||
|
import com.owncloud.android.providers.DocumentsProviderUtils.assertRegularFile
|
||||||
|
import com.owncloud.android.providers.DocumentsProviderUtils.assertRegularFolder
|
||||||
|
import com.owncloud.android.providers.DocumentsProviderUtils.findFileBlocking
|
||||||
|
import com.owncloud.android.providers.DocumentsProviderUtils.getOCFile
|
||||||
|
import com.owncloud.android.providers.DocumentsProviderUtils.listFilesBlocking
|
||||||
|
import com.owncloud.android.providers.DocumentsStorageProvider.DOCUMENTID_SEPARATOR
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import net.bytebuddy.utility.RandomString
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
private const val MAX_FILE_NAME_LENGTH = 225
|
||||||
|
|
||||||
|
class DocumentsStorageProviderIT : AbstractOnServerIT() {
|
||||||
|
|
||||||
|
private val context = targetContext
|
||||||
|
private val contentResolver = context.contentResolver
|
||||||
|
private val authority = context.getString(R.string.document_provider_authority)
|
||||||
|
|
||||||
|
private val rootFileId = storageManager.getFileByEncryptedRemotePath(ROOT_PATH).fileId
|
||||||
|
private val documentId = "${account.hashCode()}${DOCUMENTID_SEPARATOR}$rootFileId"
|
||||||
|
private val uri = DocumentsContract.buildTreeDocumentUri(authority, documentId)
|
||||||
|
private val rootDir get() = DocumentFile.fromTreeUri(context, uri)!!
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun before() {
|
||||||
|
// DocumentsProvider#onCreate() is called when the application is started
|
||||||
|
// which is *after* AbstractOnServerIT adds the accounts (when the app is freshly installed).
|
||||||
|
// So we need to query our roots here to ensure that the internal storage map is initialized.
|
||||||
|
contentResolver.query(DocumentsContract.buildRootsUri(authority), null, null, null)
|
||||||
|
assertTrue("Storage root does not exist", rootDir.exists())
|
||||||
|
assertTrue(rootDir.isDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all files in [rootDir] after each test.
|
||||||
|
*
|
||||||
|
* We can't use [AbstractOnServerIT.after] as this is only deleting remote files.
|
||||||
|
*/
|
||||||
|
@After
|
||||||
|
override fun after() = runBlocking {
|
||||||
|
rootDir.listFilesBlocking(context).forEach {
|
||||||
|
Log.e("TEST", "Deleting ${it.name}...")
|
||||||
|
it.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreateDeleteFiles() = runBlocking {
|
||||||
|
// no files in root initially
|
||||||
|
assertListFilesEquals(emptyList(), rootDir.listFilesBlocking(context))
|
||||||
|
|
||||||
|
// create first file
|
||||||
|
val name1 = RandomString.make()
|
||||||
|
val type1 = "text/html"
|
||||||
|
val file1 = rootDir.createFile(type1, name1)!!
|
||||||
|
|
||||||
|
// check assumptions
|
||||||
|
@Suppress("ForbiddenComment")
|
||||||
|
file1.assertRegularFile(name1, 0L, null/* FIXME: type1 */, rootDir)
|
||||||
|
file1.assertRecentlyModified()
|
||||||
|
|
||||||
|
// file1 is found in root
|
||||||
|
assertListFilesEquals(listOf(file1), rootDir.listFilesBlocking(context).toList())
|
||||||
|
|
||||||
|
// file1 was uploaded
|
||||||
|
val ocFile1 = file1.getOCFile(storageManager)!!
|
||||||
|
assertExistsOnServer(client, ocFile1.remotePath, true)
|
||||||
|
|
||||||
|
// create second long file with long file name
|
||||||
|
val name2 = RandomString.make(MAX_FILE_NAME_LENGTH)
|
||||||
|
val type2 = "application/octet-stream"
|
||||||
|
val file2 = rootDir.createFile(type2, name2)!!
|
||||||
|
|
||||||
|
// file2 was uploaded
|
||||||
|
val ocFile2 = file2.getOCFile(storageManager)!!
|
||||||
|
assertExistsOnServer(client, ocFile2.remotePath, true)
|
||||||
|
|
||||||
|
// check assumptions
|
||||||
|
file2.assertRegularFile(name2, 0L, type2, rootDir)
|
||||||
|
file2.assertRecentlyModified()
|
||||||
|
|
||||||
|
// both files get listed in root
|
||||||
|
assertListFilesEquals(listOf(file1, file2), rootDir.listFiles().toList())
|
||||||
|
|
||||||
|
// delete first file
|
||||||
|
assertTrue(file1.delete())
|
||||||
|
assertFalse(file1.exists())
|
||||||
|
assertExistsOnServer(client, ocFile1.remotePath, false)
|
||||||
|
|
||||||
|
// only second file gets listed in root
|
||||||
|
assertListFilesEquals(listOf(file2), rootDir.listFiles().toList())
|
||||||
|
|
||||||
|
// delete also second file
|
||||||
|
assertTrue(file2.delete())
|
||||||
|
assertFalse(file2.exists())
|
||||||
|
assertExistsOnServer(client, ocFile2.remotePath, false)
|
||||||
|
|
||||||
|
// no more files in root
|
||||||
|
assertListFilesEquals(emptyList(), rootDir.listFilesBlocking(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReadWriteFiles() {
|
||||||
|
// create random file
|
||||||
|
val file1 = rootDir.createFile("application/octet-stream", RandomString.make())!!
|
||||||
|
file1.assertRegularFile(size = 0L)
|
||||||
|
|
||||||
|
// write random bytes to file
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
val dataSize = Random.nextInt(1, 99) * 1024
|
||||||
|
val data1 = Random.nextBytes(dataSize)
|
||||||
|
contentResolver.openOutputStream(file1.uri, "wt").use {
|
||||||
|
it!!.write(data1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// read back random bytes
|
||||||
|
assertReadEquals(data1, contentResolver.openInputStream(file1.uri))
|
||||||
|
|
||||||
|
// file size was updated correctly
|
||||||
|
file1.assertRegularFile(size = data1.size.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreateDeleteFolders() = runBlocking {
|
||||||
|
// create a new folder
|
||||||
|
val dirName1 = RandomString.make()
|
||||||
|
val dir1 = rootDir.createDirectory(dirName1)!!
|
||||||
|
dir1.assertRegularFolder(dirName1, rootDir)
|
||||||
|
// FIXME about a minute gets lost somewhere after CFO sets the correct time
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
assertTrue(System.currentTimeMillis() - dir1.lastModified() < 60_000)
|
||||||
|
// dir1.assertRecentlyModified()
|
||||||
|
|
||||||
|
// ensure folder was uploaded to server
|
||||||
|
val ocDir1 = dir1.getOCFile(storageManager)!!
|
||||||
|
assertExistsOnServer(client, ocDir1.remotePath, true)
|
||||||
|
|
||||||
|
// create file in folder
|
||||||
|
val file1 = dir1.createFile("text/html", RandomString.make())!!
|
||||||
|
file1.assertRegularFile(parent = dir1)
|
||||||
|
val ocFile1 = file1.getOCFile(storageManager)!!
|
||||||
|
assertExistsOnServer(client, ocFile1.remotePath, true)
|
||||||
|
|
||||||
|
// we find the new file in the created folder and get it in the list
|
||||||
|
assertEquals(file1.uri.toString(), dir1.findFileBlocking(context, file1.name!!)!!.uri.toString())
|
||||||
|
assertListFilesEquals(listOf(file1), dir1.listFilesBlocking(context))
|
||||||
|
|
||||||
|
// delete folder
|
||||||
|
dir1.delete()
|
||||||
|
assertFalse(dir1.exists())
|
||||||
|
assertExistsOnServer(client, ocDir1.remotePath, false)
|
||||||
|
|
||||||
|
// ensure file got deleted with it
|
||||||
|
assertFalse(file1.exists())
|
||||||
|
assertExistsOnServer(client, ocFile1.remotePath, false)
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,6 +40,7 @@ import android.util.Pair;
|
||||||
|
|
||||||
import com.nextcloud.client.account.User;
|
import com.nextcloud.client.account.User;
|
||||||
import com.nextcloud.client.account.UserAccountManager;
|
import com.nextcloud.client.account.UserAccountManager;
|
||||||
|
import com.nextcloud.client.files.downloader.DownloadTask;
|
||||||
import com.owncloud.android.R;
|
import com.owncloud.android.R;
|
||||||
import com.owncloud.android.authentication.AuthenticatorActivity;
|
import com.owncloud.android.authentication.AuthenticatorActivity;
|
||||||
import com.owncloud.android.datamodel.FileDataStorageManager;
|
import com.owncloud.android.datamodel.FileDataStorageManager;
|
||||||
|
@ -55,6 +56,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCo
|
||||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||||
import com.owncloud.android.lib.resources.files.FileUtils;
|
import com.owncloud.android.lib.resources.files.FileUtils;
|
||||||
import com.owncloud.android.operations.DownloadFileOperation;
|
import com.owncloud.android.operations.DownloadFileOperation;
|
||||||
|
import com.owncloud.android.providers.DocumentsStorageProvider;
|
||||||
import com.owncloud.android.ui.activity.FileActivity;
|
import com.owncloud.android.ui.activity.FileActivity;
|
||||||
import com.owncloud.android.ui.activity.FileDisplayActivity;
|
import com.owncloud.android.ui.activity.FileDisplayActivity;
|
||||||
import com.owncloud.android.ui.dialog.SendShareDialog;
|
import com.owncloud.android.ui.dialog.SendShareDialog;
|
||||||
|
@ -497,6 +499,7 @@ public class FileDownloader extends Service
|
||||||
* Updates the OC File after a successful download.
|
* Updates the OC File after a successful download.
|
||||||
*
|
*
|
||||||
* TODO move to DownloadFileOperation
|
* TODO move to DownloadFileOperation
|
||||||
|
* unify with code from {@link DocumentsStorageProvider} and {@link DownloadTask}.
|
||||||
*/
|
*/
|
||||||
private void saveDownloadedFile() {
|
private void saveDownloadedFile() {
|
||||||
OCFile file = mStorageManager.getFileById(mCurrentDownload.getFile().getFileId());
|
OCFile file = mStorageManager.getFileById(mCurrentDownload.getFile().getFileId());
|
||||||
|
|
|
@ -1287,6 +1287,10 @@ public class UploadFileOperation extends SyncOperation {
|
||||||
if (file.fileExists()) {
|
if (file.fileExists()) {
|
||||||
file = getStorageManager().getFileById(file.getFileId());
|
file = getStorageManager().getFileById(file.getFileId());
|
||||||
}
|
}
|
||||||
|
if (file == null) {
|
||||||
|
// this can happen e.g. when the file gets deleted during upload
|
||||||
|
return;
|
||||||
|
}
|
||||||
long syncDate = System.currentTimeMillis();
|
long syncDate = System.currentTimeMillis();
|
||||||
file.setLastSyncDateForData(syncDate);
|
file.setLastSyncDateForData(syncDate);
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,6 @@ import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.res.AssetFileDescriptor;
|
import android.content.res.AssetFileDescriptor;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
|
@ -39,42 +38,44 @@ import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.CancellationSignal;
|
import android.os.CancellationSignal;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.provider.DocumentsContract;
|
import android.provider.DocumentsContract;
|
||||||
import android.provider.DocumentsProvider;
|
import android.provider.DocumentsProvider;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.nextcloud.client.account.User;
|
import com.nextcloud.client.account.User;
|
||||||
import com.nextcloud.client.account.UserAccountManager;
|
import com.nextcloud.client.account.UserAccountManager;
|
||||||
import com.nextcloud.client.account.UserAccountManagerImpl;
|
import com.nextcloud.client.account.UserAccountManagerImpl;
|
||||||
|
import com.nextcloud.client.files.downloader.DownloadTask;
|
||||||
import com.nextcloud.client.preferences.AppPreferences;
|
import com.nextcloud.client.preferences.AppPreferences;
|
||||||
import com.nextcloud.client.preferences.AppPreferencesImpl;
|
import com.nextcloud.client.preferences.AppPreferencesImpl;
|
||||||
import com.owncloud.android.MainApp;
|
import com.owncloud.android.MainApp;
|
||||||
import com.owncloud.android.R;
|
import com.owncloud.android.R;
|
||||||
import com.owncloud.android.datamodel.ArbitraryDataProvider;
|
|
||||||
import com.owncloud.android.datamodel.FileDataStorageManager;
|
import com.owncloud.android.datamodel.FileDataStorageManager;
|
||||||
import com.owncloud.android.datamodel.OCFile;
|
import com.owncloud.android.datamodel.OCFile;
|
||||||
import com.owncloud.android.datamodel.ThumbnailsCacheManager;
|
import com.owncloud.android.datamodel.ThumbnailsCacheManager;
|
||||||
import com.owncloud.android.files.services.FileDownloader;
|
import com.owncloud.android.files.services.FileDownloader;
|
||||||
|
import com.owncloud.android.files.services.FileUploader;
|
||||||
|
import com.owncloud.android.files.services.FileUploader.NameCollisionPolicy;
|
||||||
import com.owncloud.android.lib.common.OwnCloudAccount;
|
import com.owncloud.android.lib.common.OwnCloudAccount;
|
||||||
import com.owncloud.android.lib.common.OwnCloudClient;
|
import com.owncloud.android.lib.common.OwnCloudClient;
|
||||||
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
|
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
|
||||||
|
import com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException;
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
||||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||||
import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation;
|
import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation;
|
||||||
import com.owncloud.android.operations.CopyFileOperation;
|
import com.owncloud.android.operations.CopyFileOperation;
|
||||||
import com.owncloud.android.operations.CreateFolderOperation;
|
import com.owncloud.android.operations.CreateFolderOperation;
|
||||||
|
import com.owncloud.android.operations.DownloadFileOperation;
|
||||||
import com.owncloud.android.operations.MoveFileOperation;
|
import com.owncloud.android.operations.MoveFileOperation;
|
||||||
import com.owncloud.android.operations.RefreshFolderOperation;
|
import com.owncloud.android.operations.RefreshFolderOperation;
|
||||||
import com.owncloud.android.operations.RemoveFileOperation;
|
import com.owncloud.android.operations.RemoveFileOperation;
|
||||||
import com.owncloud.android.operations.RenameFileOperation;
|
import com.owncloud.android.operations.RenameFileOperation;
|
||||||
import com.owncloud.android.operations.SynchronizeFileOperation;
|
import com.owncloud.android.operations.SynchronizeFileOperation;
|
||||||
import com.owncloud.android.ui.activity.ConflictsResolveActivity;
|
|
||||||
import com.owncloud.android.ui.activity.SettingsActivity;
|
import com.owncloud.android.ui.activity.SettingsActivity;
|
||||||
import com.owncloud.android.utils.FileStorageUtils;
|
import com.owncloud.android.utils.FileStorageUtils;
|
||||||
|
import com.owncloud.android.utils.MimeTypeUtil;
|
||||||
import com.owncloud.android.utils.UriUtils;
|
import com.owncloud.android.utils.UriUtils;
|
||||||
|
|
||||||
import org.nextcloud.providers.cursors.FileCursor;
|
import org.nextcloud.providers.cursors.FileCursor;
|
||||||
|
@ -90,8 +91,12 @@ import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
|
import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
|
||||||
import static com.owncloud.android.datamodel.OCFile.ROOT_PATH;
|
import static com.owncloud.android.datamodel.OCFile.ROOT_PATH;
|
||||||
|
import static com.owncloud.android.files.services.FileUploader.LOCAL_BEHAVIOUR_MOVE;
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
public class DocumentsStorageProvider extends DocumentsProvider {
|
public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
|
@ -102,7 +107,8 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
|
|
||||||
UserAccountManager accountManager;
|
UserAccountManager accountManager;
|
||||||
|
|
||||||
private static final String DOCUMENTID_SEPARATOR = "/";
|
@VisibleForTesting
|
||||||
|
static final String DOCUMENTID_SEPARATOR = "/";
|
||||||
private static final int DOCUMENTID_PARTS = 2;
|
private static final int DOCUMENTID_PARTS = 2;
|
||||||
private final SparseArray<FileDataStorageManager> rootIdToStorageManager = new SparseArray<>();
|
private final SparseArray<FileDataStorageManager> rootIdToStorageManager = new SparseArray<>();
|
||||||
|
|
||||||
|
@ -165,13 +171,8 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
|
|
||||||
boolean isLoading = false;
|
boolean isLoading = false;
|
||||||
if (parentFolder.isExpired()) {
|
if (parentFolder.isExpired()) {
|
||||||
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(getContext().getContentResolver());
|
final ReloadFolderDocumentTask task = new ReloadFolderDocumentTask(parentFolder, result ->
|
||||||
|
getContext().getContentResolver().notifyChange(toNotifyUri(parentFolder), null, false));
|
||||||
final ReloadFolderDocumentTask task = new ReloadFolderDocumentTask(arbitraryDataProvider,
|
|
||||||
parentFolder,
|
|
||||||
result -> {
|
|
||||||
getContext().getContentResolver().notifyChange(toNotifyUri(parentFolder), null, false);
|
|
||||||
});
|
|
||||||
task.executeOnExecutor(executor);
|
task.executeOnExecutor(executor);
|
||||||
resultCursor.setLoadingTask(task);
|
resultCursor.setLoadingTask(task);
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
@ -192,108 +193,100 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
|
|
||||||
Document document = toDocument(documentId);
|
Document document = toDocument(documentId);
|
||||||
|
|
||||||
Context context = getContext();
|
Context context = getNonNullContext();
|
||||||
if (context == null) {
|
|
||||||
throw new FileNotFoundException("Context may not be null!");
|
|
||||||
}
|
|
||||||
|
|
||||||
OCFile ocFile = document.getFile();
|
OCFile ocFile = document.getFile();
|
||||||
Account account = document.getAccount();
|
Account account = document.getAccount();
|
||||||
final User user = accountManager.getUser(account.name).orElseThrow(RuntimeException::new); // should exist
|
final User user = accountManager.getUser(account.name).orElseThrow(RuntimeException::new); // should exist
|
||||||
|
|
||||||
if (!ocFile.isDown()) {
|
if (ocFile.isDown()) {
|
||||||
Intent i = new Intent(getContext(), FileDownloader.class);
|
RemoteOperationResult result;
|
||||||
i.putExtra(FileDownloader.EXTRA_USER, user);
|
|
||||||
i.putExtra(FileDownloader.EXTRA_FILE, ocFile);
|
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
|
||||||
context.startForegroundService(i);
|
|
||||||
} else {
|
|
||||||
context.startService(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
if (!waitOrGetCancelled(cancellationSignal)) {
|
|
||||||
throw new FileNotFoundException("File with id " + documentId + " not found!");
|
|
||||||
}
|
|
||||||
ocFile = document.getFile();
|
|
||||||
|
|
||||||
if (ocFile == null) {
|
|
||||||
throw new FileNotFoundException("File with id " + documentId + " not found!");
|
|
||||||
}
|
|
||||||
} while (!ocFile.isDown());
|
|
||||||
} else {
|
|
||||||
OCFile finalFile = ocFile;
|
|
||||||
Thread syncThread = new Thread(() -> {
|
|
||||||
try {
|
|
||||||
FileDataStorageManager storageManager = new FileDataStorageManager(user.toPlatformAccount(),
|
|
||||||
context.getContentResolver());
|
|
||||||
RemoteOperationResult result = new SynchronizeFileOperation(finalFile, null, user,
|
|
||||||
true, context)
|
|
||||||
.execute(storageManager, context);
|
|
||||||
if (result.getCode() == RemoteOperationResult.ResultCode.SYNC_CONFLICT) {
|
|
||||||
// ISSUE 5: if the user is not running the app (this is a service!),
|
|
||||||
// this can be very intrusive; a notification should be preferred
|
|
||||||
Intent intent = ConflictsResolveActivity.createIntent(finalFile,
|
|
||||||
user.toPlatformAccount(),
|
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK,
|
|
||||||
context);
|
|
||||||
context.startActivity(intent);
|
|
||||||
} else {
|
|
||||||
FileStorageUtils.checkIfFileFinishedSaving(finalFile);
|
|
||||||
if (!result.isSuccess()) {
|
|
||||||
showToast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log_OC.e(TAG, "Error syncing file", e);
|
|
||||||
showToast();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
syncThread.start();
|
|
||||||
try {
|
try {
|
||||||
syncThread.join();
|
result = new SynchronizeFileOperation(ocFile, null, user, true, context)
|
||||||
} catch (InterruptedException e) {
|
.execute(document.getClient(), document.getStorageManager());
|
||||||
Log.e(TAG, "Failed to wait for thread to finish", e);
|
} catch (Exception e) {
|
||||||
|
throw getFileNotFoundExceptionWithCause("Error synchronizing file: " + ocFile.getFileName(), e);
|
||||||
}
|
}
|
||||||
|
if (result.getCode() == RemoteOperationResult.ResultCode.SYNC_CONFLICT) {
|
||||||
|
// TODO show a conflict notification with a pending intent that shows a ConflictResolveDialog
|
||||||
|
Log_OC.w(TAG, "Conflict found: " + result);
|
||||||
|
} else if (!result.isSuccess()) {
|
||||||
|
Log_OC.e(TAG, result.toString());
|
||||||
|
throw new FileNotFoundException("Error synchronizing file: " + ocFile.getFileName());
|
||||||
|
}
|
||||||
|
// TODO test if this needed here
|
||||||
|
// block thread until file is saved
|
||||||
|
FileStorageUtils.checkIfFileFinishedSaving(ocFile);
|
||||||
|
} else {
|
||||||
|
DownloadFileOperation downloadFileOperation = new DownloadFileOperation(account, ocFile, context);
|
||||||
|
RemoteOperationResult result = downloadFileOperation.execute(document.getClient());
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
Log_OC.e(TAG, result.toString());
|
||||||
|
throw new FileNotFoundException("Error downloading file: " + ocFile.getFileName());
|
||||||
|
}
|
||||||
|
saveDownloadedFile(document.getStorageManager(), downloadFileOperation, ocFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
File file = new File(ocFile.getStoragePath());
|
File file = new File(ocFile.getStoragePath());
|
||||||
int accessMode = ParcelFileDescriptor.parseMode(mode);
|
int accessMode = ParcelFileDescriptor.parseMode(mode);
|
||||||
boolean isWrite = mode.indexOf('w') != -1;
|
boolean isWrite = accessMode != ParcelFileDescriptor.MODE_READ_ONLY;
|
||||||
|
|
||||||
final OCFile oldFile = ocFile;
|
|
||||||
final OCFile newFile = ocFile;
|
|
||||||
|
|
||||||
if (isWrite) {
|
if (isWrite) {
|
||||||
|
// The calling thread is not guaranteed to have a Looper, so we can't block it with the OnCloseListener.
|
||||||
|
// Thus, we are unable to do a synchronous upload and have to start an asynchronous one.
|
||||||
|
Handler handler = new Handler(context.getMainLooper());
|
||||||
try {
|
try {
|
||||||
// reset last sync date to ensure we will be syncing this write to the server
|
return ParcelFileDescriptor.open(file, accessMode, handler, error -> {
|
||||||
ocFile.setLastSyncDateForData(0);
|
if (error == null) { // no error
|
||||||
Handler handler = new Handler(context.getMainLooper());
|
// As we can't upload the file synchronously, let's at least update its metadata here already.
|
||||||
return ParcelFileDescriptor.open(file, accessMode, handler, l -> {
|
ocFile.setFileLength(file.length());
|
||||||
RemoteOperationResult result = new SynchronizeFileOperation(newFile, oldFile, user, true,
|
ocFile.setModificationTimestamp(System.currentTimeMillis());
|
||||||
context)
|
document.getStorageManager().saveFile(ocFile);
|
||||||
.execute(document.getClient(), document.getStorageManager());
|
|
||||||
|
|
||||||
boolean success = result.isSuccess();
|
// TODO disable upload notifications as DocumentsProvider users already show them
|
||||||
|
// upload file with FileUploader service (off main thread)
|
||||||
if (!success) {
|
FileUploader.uploadUpdateFile(
|
||||||
Log_OC.e(TAG, "Failed to update document with id " + documentId);
|
context,
|
||||||
|
account,
|
||||||
|
ocFile,
|
||||||
|
LOCAL_BEHAVIOUR_MOVE,
|
||||||
|
NameCollisionPolicy.OVERWRITE
|
||||||
|
);
|
||||||
|
} else { // error, no upload needed
|
||||||
|
Log_OC.e(TAG, "File was closed with an error: " + ocFile.getFileName(), error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new FileNotFoundException("Failed to open/edit document with id " + documentId);
|
throw new FileNotFoundException("Failed to open document for writing " + ocFile.getFileName());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return ParcelFileDescriptor.open(file, accessMode);
|
return ParcelFileDescriptor.open(file, accessMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showToast() {
|
/**
|
||||||
Handler handler = new Handler(Looper.getMainLooper());
|
* Updates the OC File after a successful download.
|
||||||
handler.post(() -> Toast.makeText(MainApp.getAppContext(),
|
*
|
||||||
R.string.file_not_synced,
|
* TODO unify with code from {@link FileDownloader} and {@link DownloadTask}.
|
||||||
Toast.LENGTH_SHORT).show());
|
*/
|
||||||
|
private void saveDownloadedFile(FileDataStorageManager storageManager, DownloadFileOperation dfo, OCFile file) {
|
||||||
|
long syncDate = System.currentTimeMillis();
|
||||||
|
file.setLastSyncDateForProperties(syncDate);
|
||||||
|
file.setLastSyncDateForData(syncDate);
|
||||||
|
file.setUpdateThumbnailNeeded(true);
|
||||||
|
file.setModificationTimestamp(dfo.getModificationTimestamp());
|
||||||
|
file.setModificationTimestampAtLastSyncForData(dfo.getModificationTimestamp());
|
||||||
|
file.setEtag(dfo.getEtag());
|
||||||
|
file.setMimeType(dfo.getMimeType());
|
||||||
|
String savePath = dfo.getSavePath();
|
||||||
|
file.setStoragePath(savePath);
|
||||||
|
file.setFileLength(new File(savePath).length());
|
||||||
|
file.setRemoteId(dfo.getFile().getRemoteId());
|
||||||
|
storageManager.saveFile(file);
|
||||||
|
if (MimeTypeUtil.isMedia(dfo.getMimeType())) {
|
||||||
|
FileDataStorageManager.triggerMediaScan(file.getStoragePath(), file);
|
||||||
|
}
|
||||||
|
storageManager.saveConflict(file, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -349,6 +342,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
.execute(document.getClient(), document.getStorageManager());
|
.execute(document.getClient(), document.getStorageManager());
|
||||||
|
|
||||||
if (!result.isSuccess()) {
|
if (!result.isSuccess()) {
|
||||||
|
Log_OC.e(TAG, result.toString());
|
||||||
throw new FileNotFoundException("Failed to rename document with documentId " + documentId + ": " +
|
throw new FileNotFoundException("Failed to rename document with documentId " + documentId + ": " +
|
||||||
result.getException());
|
result.getException());
|
||||||
}
|
}
|
||||||
|
@ -376,6 +370,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
.execute(document.getClient(), storageManager);
|
.execute(document.getClient(), storageManager);
|
||||||
|
|
||||||
if (!result.isSuccess()) {
|
if (!result.isSuccess()) {
|
||||||
|
Log_OC.e(TAG, result.toString());
|
||||||
throw new FileNotFoundException("Failed to copy document with documentId " + sourceDocumentId
|
throw new FileNotFoundException("Failed to copy document with documentId " + sourceDocumentId
|
||||||
+ " to " + targetParentDocumentId);
|
+ " to " + targetParentDocumentId);
|
||||||
}
|
}
|
||||||
|
@ -388,6 +383,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
.execute(targetFolder.getClient());
|
.execute(targetFolder.getClient());
|
||||||
|
|
||||||
if (!updateParent.isSuccess()) {
|
if (!updateParent.isSuccess()) {
|
||||||
|
Log_OC.e(TAG, updateParent.toString());
|
||||||
throw new FileNotFoundException("Failed to copy document with documentId " + sourceDocumentId
|
throw new FileNotFoundException("Failed to copy document with documentId " + sourceDocumentId
|
||||||
+ " to " + targetParentDocumentId);
|
+ " to " + targetParentDocumentId);
|
||||||
}
|
}
|
||||||
|
@ -421,6 +417,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
.execute(document.getClient(), document.getStorageManager());
|
.execute(document.getClient(), document.getStorageManager());
|
||||||
|
|
||||||
if (!result.isSuccess()) {
|
if (!result.isSuccess()) {
|
||||||
|
Log_OC.e(TAG, result.toString());
|
||||||
throw new FileNotFoundException("Failed to move document with documentId " + sourceDocumentId
|
throw new FileNotFoundException("Failed to move document with documentId " + sourceDocumentId
|
||||||
+ " to " + targetParentDocumentId);
|
+ " to " + targetParentDocumentId);
|
||||||
}
|
}
|
||||||
|
@ -460,7 +457,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
if (DocumentsContract.Document.MIME_TYPE_DIR.equalsIgnoreCase(mimeType)) {
|
if (DocumentsContract.Document.MIME_TYPE_DIR.equalsIgnoreCase(mimeType)) {
|
||||||
return createFolder(folderDocument, displayName);
|
return createFolder(folderDocument, displayName);
|
||||||
} else {
|
} else {
|
||||||
return createFile(folderDocument, displayName);
|
return createFile(folderDocument, displayName, mimeType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -481,6 +478,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
.execute(targetFolder.getClient(), storageManager);
|
.execute(targetFolder.getClient(), storageManager);
|
||||||
|
|
||||||
if (!result.isSuccess()) {
|
if (!result.isSuccess()) {
|
||||||
|
Log_OC.e(TAG, result.toString());
|
||||||
throw new FileNotFoundException("Failed to create document with name " +
|
throw new FileNotFoundException("Failed to create document with name " +
|
||||||
displayName + " and documentId " + targetFolder.getDocumentId());
|
displayName + " and documentId " + targetFolder.getDocumentId());
|
||||||
}
|
}
|
||||||
|
@ -491,6 +489,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
.execute(targetFolder.getClient());
|
.execute(targetFolder.getClient());
|
||||||
|
|
||||||
if (!updateParent.isSuccess()) {
|
if (!updateParent.isSuccess()) {
|
||||||
|
Log_OC.e(TAG, updateParent.toString());
|
||||||
throw new FileNotFoundException("Failed to create document with documentId " + targetFolder.getDocumentId());
|
throw new FileNotFoundException("Failed to create document with documentId " + targetFolder.getDocumentId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -501,7 +500,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
return newFolder.getDocumentId();
|
return newFolder.getDocumentId();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createFile(Document targetFolder, String displayName) throws FileNotFoundException {
|
private String createFile(Document targetFolder, String displayName, String mimeType) throws FileNotFoundException {
|
||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
throw new FileNotFoundException("Context may not be null!");
|
throw new FileNotFoundException("Context may not be null!");
|
||||||
|
@ -527,21 +526,24 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
throw new FileNotFoundException("File could not be created");
|
throw new FileNotFoundException("File could not be created");
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new FileNotFoundException("File could not be created");
|
throw getFileNotFoundExceptionWithCause("File could not be created", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
String newFilePath = targetFolder.getRemotePath() + displayName;
|
String newFilePath = targetFolder.getRemotePath() + displayName;
|
||||||
|
|
||||||
|
// FIXME we need to update the mimeType somewhere else as well
|
||||||
|
|
||||||
// perform the upload, no need for chunked operation as we have a empty file
|
// perform the upload, no need for chunked operation as we have a empty file
|
||||||
OwnCloudClient client = targetFolder.getClient();
|
OwnCloudClient client = targetFolder.getClient();
|
||||||
RemoteOperationResult result = new UploadFileRemoteOperation(emptyFile.getAbsolutePath(),
|
RemoteOperationResult result = new UploadFileRemoteOperation(emptyFile.getAbsolutePath(),
|
||||||
newFilePath,
|
newFilePath,
|
||||||
null,
|
mimeType,
|
||||||
"",
|
"",
|
||||||
String.valueOf(System.currentTimeMillis() / 1000))
|
String.valueOf(System.currentTimeMillis() / 1000))
|
||||||
.execute(client);
|
.execute(client);
|
||||||
|
|
||||||
if (!result.isSuccess()) {
|
if (!result.isSuccess()) {
|
||||||
|
Log_OC.e(TAG, result.toString());
|
||||||
throw new FileNotFoundException("Failed to upload document with path " + newFilePath);
|
throw new FileNotFoundException("Failed to upload document with path " + newFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -556,6 +558,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
.execute(client);
|
.execute(client);
|
||||||
|
|
||||||
if (!updateParent.isSuccess()) {
|
if (!updateParent.isSuccess()) {
|
||||||
|
Log_OC.e(TAG, updateParent.toString());
|
||||||
throw new FileNotFoundException("Failed to create document with documentId " + targetFolder.getDocumentId());
|
throw new FileNotFoundException("Failed to create document with documentId " + targetFolder.getDocumentId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -649,6 +652,12 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private FileNotFoundException getFileNotFoundExceptionWithCause(String msg, Exception cause) {
|
||||||
|
FileNotFoundException e = new FileNotFoundException(msg);
|
||||||
|
e.initCause(cause);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
private FileDataStorageManager getStorageManager(String rootId) {
|
private FileDataStorageManager getStorageManager(String rootId) {
|
||||||
for(int i = 0; i < rootIdToStorageManager.size(); i++) {
|
for(int i = 0; i < rootIdToStorageManager.size(); i++) {
|
||||||
FileDataStorageManager storageManager = rootIdToStorageManager.valueAt(i);
|
FileDataStorageManager storageManager = rootIdToStorageManager.valueAt(i);
|
||||||
|
@ -672,16 +681,6 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean waitOrGetCancelled(CancellationSignal cancellationSignal) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(1000);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !(cancellationSignal != null && cancellationSignal.isCanceled());
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Document> findFiles(Document root, String query) {
|
private List<Document> findFiles(Document root, String query) {
|
||||||
FileDataStorageManager storageManager = root.getStorageManager();
|
FileDataStorageManager storageManager = root.getStorageManager();
|
||||||
List<Document> result = new ArrayList<>();
|
List<Document> result = new ArrayList<>();
|
||||||
|
@ -715,6 +714,20 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
return new Document(storageManager, Long.parseLong(separated[1]));
|
return new Document(storageManager, Long.parseLong(separated[1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link Context} guaranteed to be non-null.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if called before {@link #onCreate()}.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private Context getNonNullContext() {
|
||||||
|
Context context = getContext();
|
||||||
|
if (context == null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
public interface OnTaskFinishedCallback {
|
public interface OnTaskFinishedCallback {
|
||||||
void onTaskFinished(RemoteOperationResult result);
|
void onTaskFinished(RemoteOperationResult result);
|
||||||
}
|
}
|
||||||
|
@ -723,14 +736,10 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
|
|
||||||
private final Document folder;
|
private final Document folder;
|
||||||
private final OnTaskFinishedCallback callback;
|
private final OnTaskFinishedCallback callback;
|
||||||
private final ArbitraryDataProvider arbitraryDataProvider;
|
|
||||||
|
|
||||||
ReloadFolderDocumentTask(ArbitraryDataProvider arbitraryDataProvider,
|
ReloadFolderDocumentTask(Document folder, OnTaskFinishedCallback callback) {
|
||||||
Document folder,
|
|
||||||
OnTaskFinishedCallback callback) {
|
|
||||||
this.folder = folder;
|
this.folder = folder;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.arbitraryDataProvider = arbitraryDataProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -798,8 +807,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
|
||||||
try {
|
try {
|
||||||
OwnCloudAccount ocAccount = new OwnCloudAccount(getAccount(), MainApp.getAppContext());
|
OwnCloudAccount ocAccount = new OwnCloudAccount(getAccount(), MainApp.getAppContext());
|
||||||
return OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, getContext());
|
return OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, getContext());
|
||||||
} catch (OperationCanceledException | IOException | AuthenticatorException |
|
} catch (OperationCanceledException | IOException | AuthenticatorException | AccountNotFoundException e) {
|
||||||
com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) {
|
|
||||||
Log_OC.e(TAG, "Failed to set client", e);
|
Log_OC.e(TAG, "Failed to set client", e);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
Loading…
Reference in a new issue