mirror of
https://github.com/nextcloud/android.git
synced 2024-11-23 13:45:35 +03:00
File export aka "download"
Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
parent
6441e5d189
commit
1d69dcfcaa
20 changed files with 718 additions and 162 deletions
|
@ -426,13 +426,6 @@ public class UploadIT extends AbstractOnServerIT {
|
||||||
String remotePath = "/testFile.txt";
|
String remotePath = "/testFile.txt";
|
||||||
OCUpload ocUpload = new OCUpload(file.getAbsolutePath(), remotePath, account.name);
|
OCUpload ocUpload = new OCUpload(file.getAbsolutePath(), remotePath, account.name);
|
||||||
|
|
||||||
long creationTimestamp = Files.readAttributes(file.toPath(), BasicFileAttributes.class)
|
|
||||||
.creationTime()
|
|
||||||
.to(TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
// wait a bit to simulate a later upload, so we can verify if creation date is set correct
|
|
||||||
shortSleep();
|
|
||||||
|
|
||||||
assertTrue(
|
assertTrue(
|
||||||
new UploadFileOperation(
|
new UploadFileOperation(
|
||||||
uploadsStorageManager,
|
uploadsStorageManager,
|
||||||
|
@ -453,6 +446,10 @@ public class UploadIT extends AbstractOnServerIT {
|
||||||
.isSuccess()
|
.isSuccess()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
long creationTimestamp = Files.readAttributes(file.toPath(), BasicFileAttributes.class)
|
||||||
|
.creationTime()
|
||||||
|
.to(TimeUnit.SECONDS);
|
||||||
|
|
||||||
long uploadTimestamp = System.currentTimeMillis() / 1000;
|
long uploadTimestamp = System.currentTimeMillis() / 1000;
|
||||||
|
|
||||||
// RefreshFolderOperation
|
// RefreshFolderOperation
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* Nextcloud Android client application
|
||||||
|
*
|
||||||
|
* @author Tobias Kaminsky
|
||||||
|
* Copyright (C) 2022 Tobias Kaminsky
|
||||||
|
* Copyright (C) 2022 Nextcloud GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.owncloud.android.utils
|
||||||
|
|
||||||
|
import com.owncloud.android.AbstractIT
|
||||||
|
import com.owncloud.android.datamodel.OCFile
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class FileExportUtilsIT : AbstractIT() {
|
||||||
|
@Test
|
||||||
|
fun exportFile() {
|
||||||
|
val file = createFile("export.txt", 10)
|
||||||
|
|
||||||
|
val sut = FileExportUtils()
|
||||||
|
|
||||||
|
val expectedFile = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||||
|
File("/sdcard/Downloads/export.txt")
|
||||||
|
} else {
|
||||||
|
File("/storage/emulated/0/Download/export.txt")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse(expectedFile.exists())
|
||||||
|
|
||||||
|
sut.exportFile("export.txt", "/text/plain", targetContext.contentResolver, null, file)
|
||||||
|
|
||||||
|
assertTrue(expectedFile.exists())
|
||||||
|
assertEquals(file.length(), expectedFile.length())
|
||||||
|
assertTrue(expectedFile.delete())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun exportOCFile() {
|
||||||
|
val file = createFile("export.txt", 10)
|
||||||
|
val ocFile = OCFile("/export.txt").apply {
|
||||||
|
storagePath = file.absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
val sut = FileExportUtils()
|
||||||
|
|
||||||
|
val expectedFile = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||||
|
File("/sdcard/Downloads/export.txt")
|
||||||
|
} else {
|
||||||
|
File("/storage/emulated/0/Download/export.txt")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse(expectedFile.exists())
|
||||||
|
|
||||||
|
sut.exportFile("export.txt", "/text/plain", targetContext.contentResolver, ocFile, null)
|
||||||
|
|
||||||
|
assertTrue(expectedFile.exists())
|
||||||
|
assertEquals(file.length(), expectedFile.length())
|
||||||
|
assertTrue(expectedFile.delete())
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,6 @@
|
||||||
* @author Tobias Kaminsky
|
* @author Tobias Kaminsky
|
||||||
* Copyright (C) 2020 Tobias Kaminsky
|
* Copyright (C) 2020 Tobias Kaminsky
|
||||||
* Copyright (C) 2020 Nextcloud GmbH
|
* Copyright (C) 2020 Nextcloud GmbH
|
||||||
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
@ -20,54 +19,21 @@
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package com.owncloud.android.ui.fragment
|
package com.owncloud.android.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.espresso.intent.rule.IntentsTestRule
|
import com.owncloud.android.AbstractIT
|
||||||
import com.nextcloud.client.GrantStoragePermissionRule
|
|
||||||
import com.nextcloud.client.device.BatteryStatus
|
|
||||||
import com.nextcloud.client.device.PowerManagementService
|
|
||||||
import com.nextcloud.client.network.Connectivity
|
|
||||||
import com.nextcloud.client.network.ConnectivityService
|
|
||||||
import com.owncloud.android.AbstractOnServerIT
|
|
||||||
import com.owncloud.android.datamodel.OCFile
|
import com.owncloud.android.datamodel.OCFile
|
||||||
import com.owncloud.android.ui.activity.FileDisplayActivity
|
import com.owncloud.android.utils.FileStorageUtils.checkIfEnoughSpace
|
||||||
|
import com.owncloud.android.utils.FileStorageUtils.pathToUserFriendlyDisplay
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.rules.TestRule
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class OCFileListFragmentIT : AbstractOnServerIT() {
|
class FileStorageUtilsIT : AbstractIT() {
|
||||||
@get:Rule
|
|
||||||
val activityRule = IntentsTestRule(FileDisplayActivity::class.java, true, false)
|
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
val permissionRule: TestRule = GrantStoragePermissionRule.grant()
|
|
||||||
|
|
||||||
private val connectivityServiceMock: ConnectivityService = object : ConnectivityService {
|
|
||||||
override fun isInternetWalled(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getConnectivity(): Connectivity {
|
|
||||||
return Connectivity.CONNECTED_WIFI
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val powerManagementServiceMock: PowerManagementService = object : PowerManagementService {
|
|
||||||
override val isPowerSavingEnabled: Boolean
|
|
||||||
get() = false
|
|
||||||
|
|
||||||
override val isPowerSavingExclusionAvailable: Boolean
|
|
||||||
get() = false
|
|
||||||
|
|
||||||
override val battery: BatteryStatus
|
|
||||||
get() = BatteryStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openFile(name: String): File {
|
private fun openFile(name: String): File {
|
||||||
val ctx: Context = ApplicationProvider.getApplicationContext()
|
val ctx: Context = ApplicationProvider.getApplicationContext()
|
||||||
val externalFilesDir = ctx.getExternalFilesDir(null)
|
val externalFilesDir = ctx.getExternalFilesDir(null)
|
||||||
|
@ -77,7 +43,6 @@ class OCFileListFragmentIT : AbstractOnServerIT() {
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("MagicNumber")
|
@SuppressWarnings("MagicNumber")
|
||||||
fun testEnoughSpaceWithoutLocalFile() {
|
fun testEnoughSpaceWithoutLocalFile() {
|
||||||
val sut = OCFileListFragment()
|
|
||||||
val ocFile = OCFile("/test.txt")
|
val ocFile = OCFile("/test.txt")
|
||||||
val file = openFile("test.txt")
|
val file = openFile("test.txt")
|
||||||
file.createNewFile()
|
file.createNewFile()
|
||||||
|
@ -85,22 +50,21 @@ class OCFileListFragmentIT : AbstractOnServerIT() {
|
||||||
ocFile.storagePath = file.absolutePath
|
ocFile.storagePath = file.absolutePath
|
||||||
|
|
||||||
ocFile.fileLength = 100
|
ocFile.fileLength = 100
|
||||||
assertTrue(sut.checkIfEnoughSpace(200L, ocFile))
|
assertTrue(checkIfEnoughSpace(200L, ocFile))
|
||||||
|
|
||||||
ocFile.fileLength = 0
|
ocFile.fileLength = 0
|
||||||
assertTrue(sut.checkIfEnoughSpace(200L, ocFile))
|
assertTrue(checkIfEnoughSpace(200L, ocFile))
|
||||||
|
|
||||||
ocFile.fileLength = 100
|
ocFile.fileLength = 100
|
||||||
assertFalse(sut.checkIfEnoughSpace(50L, ocFile))
|
assertFalse(checkIfEnoughSpace(50L, ocFile))
|
||||||
|
|
||||||
ocFile.fileLength = 100
|
ocFile.fileLength = 100
|
||||||
assertFalse(sut.checkIfEnoughSpace(100L, ocFile))
|
assertFalse(checkIfEnoughSpace(100L, ocFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("MagicNumber")
|
@SuppressWarnings("MagicNumber")
|
||||||
fun testEnoughSpaceWithLocalFile() {
|
fun testEnoughSpaceWithLocalFile() {
|
||||||
val sut = OCFileListFragment()
|
|
||||||
val ocFile = OCFile("/test.txt")
|
val ocFile = OCFile("/test.txt")
|
||||||
val file = openFile("test.txt")
|
val file = openFile("test.txt")
|
||||||
file.writeText("123123")
|
file.writeText("123123")
|
||||||
|
@ -108,22 +72,21 @@ class OCFileListFragmentIT : AbstractOnServerIT() {
|
||||||
ocFile.storagePath = file.absolutePath
|
ocFile.storagePath = file.absolutePath
|
||||||
|
|
||||||
ocFile.fileLength = 100
|
ocFile.fileLength = 100
|
||||||
assertTrue(sut.checkIfEnoughSpace(200L, ocFile))
|
assertTrue(checkIfEnoughSpace(200L, ocFile))
|
||||||
|
|
||||||
ocFile.fileLength = 0
|
ocFile.fileLength = 0
|
||||||
assertTrue(sut.checkIfEnoughSpace(200L, ocFile))
|
assertTrue(checkIfEnoughSpace(200L, ocFile))
|
||||||
|
|
||||||
ocFile.fileLength = 100
|
ocFile.fileLength = 100
|
||||||
assertFalse(sut.checkIfEnoughSpace(50L, ocFile))
|
assertFalse(checkIfEnoughSpace(50L, ocFile))
|
||||||
|
|
||||||
ocFile.fileLength = 100
|
ocFile.fileLength = 100
|
||||||
assertFalse(sut.checkIfEnoughSpace(100L, ocFile))
|
assertFalse(checkIfEnoughSpace(100L, ocFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("MagicNumber")
|
@SuppressWarnings("MagicNumber")
|
||||||
fun testEnoughSpaceWithoutLocalFolder() {
|
fun testEnoughSpaceWithoutLocalFolder() {
|
||||||
val sut = OCFileListFragment()
|
|
||||||
val ocFile = OCFile("/test/")
|
val ocFile = OCFile("/test/")
|
||||||
val file = openFile("test")
|
val file = openFile("test")
|
||||||
File(file, "1.txt").writeText("123123")
|
File(file, "1.txt").writeText("123123")
|
||||||
|
@ -131,22 +94,21 @@ class OCFileListFragmentIT : AbstractOnServerIT() {
|
||||||
ocFile.storagePath = file.absolutePath
|
ocFile.storagePath = file.absolutePath
|
||||||
|
|
||||||
ocFile.fileLength = 100
|
ocFile.fileLength = 100
|
||||||
assertTrue(sut.checkIfEnoughSpace(200L, ocFile))
|
assertTrue(checkIfEnoughSpace(200L, ocFile))
|
||||||
|
|
||||||
ocFile.fileLength = 0
|
ocFile.fileLength = 0
|
||||||
assertTrue(sut.checkIfEnoughSpace(200L, ocFile))
|
assertTrue(checkIfEnoughSpace(200L, ocFile))
|
||||||
|
|
||||||
ocFile.fileLength = 100
|
ocFile.fileLength = 100
|
||||||
assertFalse(sut.checkIfEnoughSpace(50L, ocFile))
|
assertFalse(checkIfEnoughSpace(50L, ocFile))
|
||||||
|
|
||||||
ocFile.fileLength = 100
|
ocFile.fileLength = 100
|
||||||
assertFalse(sut.checkIfEnoughSpace(100L, ocFile))
|
assertFalse(checkIfEnoughSpace(100L, ocFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("MagicNumber")
|
@SuppressWarnings("MagicNumber")
|
||||||
fun testEnoughSpaceWithLocalFolder() {
|
fun testEnoughSpaceWithLocalFolder() {
|
||||||
val sut = OCFileListFragment()
|
|
||||||
val ocFile = OCFile("/test/")
|
val ocFile = OCFile("/test/")
|
||||||
val folder = openFile("test")
|
val folder = openFile("test")
|
||||||
folder.mkdirs()
|
folder.mkdirs()
|
||||||
|
@ -158,30 +120,42 @@ class OCFileListFragmentIT : AbstractOnServerIT() {
|
||||||
ocFile.mimeType = "DIR"
|
ocFile.mimeType = "DIR"
|
||||||
|
|
||||||
ocFile.fileLength = 100
|
ocFile.fileLength = 100
|
||||||
assertTrue(sut.checkIfEnoughSpace(200L, ocFile))
|
assertTrue(checkIfEnoughSpace(200L, ocFile))
|
||||||
|
|
||||||
ocFile.fileLength = 0
|
ocFile.fileLength = 0
|
||||||
assertTrue(sut.checkIfEnoughSpace(200L, ocFile))
|
assertTrue(checkIfEnoughSpace(200L, ocFile))
|
||||||
|
|
||||||
ocFile.fileLength = 100
|
ocFile.fileLength = 100
|
||||||
assertFalse(sut.checkIfEnoughSpace(50L, ocFile))
|
assertFalse(checkIfEnoughSpace(50L, ocFile))
|
||||||
|
|
||||||
ocFile.fileLength = 44
|
ocFile.fileLength = 44
|
||||||
assertTrue(sut.checkIfEnoughSpace(50L, ocFile))
|
assertTrue(checkIfEnoughSpace(50L, ocFile))
|
||||||
|
|
||||||
ocFile.fileLength = 100
|
ocFile.fileLength = 100
|
||||||
assertTrue(sut.checkIfEnoughSpace(100L, ocFile))
|
assertTrue(checkIfEnoughSpace(100L, ocFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("MagicNumber")
|
@SuppressWarnings("MagicNumber")
|
||||||
fun testEnoughSpaceWithNoLocalFolder() {
|
fun testEnoughSpaceWithNoLocalFolder() {
|
||||||
val sut = OCFileListFragment()
|
|
||||||
val ocFile = OCFile("/test/")
|
val ocFile = OCFile("/test/")
|
||||||
|
|
||||||
ocFile.mimeType = "DIR"
|
ocFile.mimeType = "DIR"
|
||||||
|
|
||||||
ocFile.fileLength = 100
|
ocFile.fileLength = 100
|
||||||
assertTrue(sut.checkIfEnoughSpace(200L, ocFile))
|
assertTrue(checkIfEnoughSpace(200L, ocFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPathToUserFriendlyDisplay() {
|
||||||
|
assertEquals("/", pathToUserFriendlyDisplay("/"))
|
||||||
|
assertEquals("/sdcard/", pathToUserFriendlyDisplay("/sdcard/"))
|
||||||
|
assertEquals("/sdcard/test/1/", pathToUserFriendlyDisplay("/sdcard/test/1/"))
|
||||||
|
assertEquals("Internal storage/Movies/", pathToUserFriendlyDisplay("/storage/emulated/0/Movies/"))
|
||||||
|
assertEquals("Internal storage/", pathToUserFriendlyDisplay("/storage/emulated/0/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pathToUserFriendlyDisplay(path: String): String {
|
||||||
|
return pathToUserFriendlyDisplay(path, targetContext, targetContext.resources)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
*
|
|
||||||
* Nextcloud Android client application
|
|
||||||
*
|
|
||||||
* @author Tobias Kaminsky
|
|
||||||
* Copyright (C) 2020 Tobias Kaminsky
|
|
||||||
* Copyright (C) 2020 Nextcloud GmbH
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.owncloud.android.utils
|
|
||||||
|
|
||||||
import com.owncloud.android.AbstractIT
|
|
||||||
import com.owncloud.android.utils.FileStorageUtils.pathToUserFriendlyDisplay
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class FileStorageUtilsTest : AbstractIT() {
|
|
||||||
@Test
|
|
||||||
fun testPathToUserFriendlyDisplay() {
|
|
||||||
assertEquals("/", pathToUserFriendlyDisplay("/"))
|
|
||||||
assertEquals("/sdcard/", pathToUserFriendlyDisplay("/sdcard/"))
|
|
||||||
assertEquals("/sdcard/test/1/", pathToUserFriendlyDisplay("/sdcard/test/1/"))
|
|
||||||
assertEquals("Internal storage/Movies/", pathToUserFriendlyDisplay("/storage/emulated/0/Movies/"))
|
|
||||||
assertEquals("Internal storage/", pathToUserFriendlyDisplay("/storage/emulated/0/"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pathToUserFriendlyDisplay(path: String): String {
|
|
||||||
return pathToUserFriendlyDisplay(path, targetContext, targetContext.resources)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -156,6 +156,9 @@
|
||||||
<receiver
|
<receiver
|
||||||
android:name="com.nextcloud.client.jobs.NotificationWork$NotificationReceiver"
|
android:name="com.nextcloud.client.jobs.NotificationWork$NotificationReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<receiver
|
||||||
|
android:name="com.nextcloud.client.jobs.FilesExportWork$NotificationReceiver"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.activity.UploadFilesActivity"
|
android:name=".ui.activity.UploadFilesActivity"
|
||||||
|
|
|
@ -100,11 +100,25 @@ class BackgroundJobFactory @Inject constructor(
|
||||||
AccountRemovalWork::class -> createAccountRemovalWork(context, workerParameters)
|
AccountRemovalWork::class -> createAccountRemovalWork(context, workerParameters)
|
||||||
CalendarBackupWork::class -> createCalendarBackupWork(context, workerParameters)
|
CalendarBackupWork::class -> createCalendarBackupWork(context, workerParameters)
|
||||||
CalendarImportWork::class -> createCalendarImportWork(context, workerParameters)
|
CalendarImportWork::class -> createCalendarImportWork(context, workerParameters)
|
||||||
|
FilesExportWork::class -> createFilesDownloadWork(context, workerParameters)
|
||||||
else -> null // caller falls back to default factory
|
else -> null // caller falls back to default factory
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createFilesDownloadWork(
|
||||||
|
context: Context,
|
||||||
|
params: WorkerParameters
|
||||||
|
): ListenableWorker {
|
||||||
|
return FilesExportWork(
|
||||||
|
context,
|
||||||
|
accountManager.user,
|
||||||
|
contentResolver,
|
||||||
|
themeColorUtils,
|
||||||
|
params
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun createContentObserverJob(
|
private fun createContentObserverJob(
|
||||||
context: Context,
|
context: Context,
|
||||||
workerParameters: WorkerParameters,
|
workerParameters: WorkerParameters,
|
||||||
|
|
|
@ -23,6 +23,7 @@ import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.nextcloud.client.account.User
|
import com.nextcloud.client.account.User
|
||||||
|
import com.owncloud.android.datamodel.OCFile
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface allows to control, schedule and monitor all application
|
* This interface allows to control, schedule and monitor all application
|
||||||
|
@ -126,6 +127,8 @@ interface BackgroundJobManager {
|
||||||
*/
|
*/
|
||||||
fun startImmediateCalendarImport(calendarPaths: Map<String, Int>): LiveData<JobInfo?>
|
fun startImmediateCalendarImport(calendarPaths: Map<String, Int>): LiveData<JobInfo?>
|
||||||
|
|
||||||
|
fun startImmediateFilesDownloadJob(files: Collection<OCFile>): LiveData<JobInfo?>
|
||||||
|
|
||||||
fun schedulePeriodicFilesSyncJob()
|
fun schedulePeriodicFilesSyncJob()
|
||||||
fun startImmediateFilesSyncJob(skipCustomFolders: Boolean = false, overridePowerSaving: Boolean = false)
|
fun startImmediateFilesSyncJob(skipCustomFolders: Boolean = false, overridePowerSaving: Boolean = false)
|
||||||
fun scheduleOfflineSync()
|
fun scheduleOfflineSync()
|
||||||
|
|
|
@ -37,6 +37,7 @@ import androidx.work.WorkInfo
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.nextcloud.client.account.User
|
import com.nextcloud.client.account.User
|
||||||
import com.nextcloud.client.core.Clock
|
import com.nextcloud.client.core.Clock
|
||||||
|
import com.owncloud.android.datamodel.OCFile
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -78,6 +79,7 @@ internal class BackgroundJobManagerImpl(
|
||||||
const val JOB_NOTIFICATION = "notification"
|
const val JOB_NOTIFICATION = "notification"
|
||||||
const val JOB_ACCOUNT_REMOVAL = "account_removal"
|
const val JOB_ACCOUNT_REMOVAL = "account_removal"
|
||||||
const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup"
|
const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup"
|
||||||
|
const val JOB_IMMEDIATE_FILES_DOWNLOAD = "immediate_files_download"
|
||||||
|
|
||||||
const val JOB_TEST = "test_job"
|
const val JOB_TEST = "test_job"
|
||||||
|
|
||||||
|
@ -298,6 +300,22 @@ internal class BackgroundJobManagerImpl(
|
||||||
return workManager.getJobInfo(request.id)
|
return workManager.getJobInfo(request.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun startImmediateFilesDownloadJob(files: Collection<OCFile>): LiveData<JobInfo?> {
|
||||||
|
val ids = files.map { it.fileId }.toLongArray()
|
||||||
|
|
||||||
|
val data = Data.Builder()
|
||||||
|
.putLongArray(FilesExportWork.FILES_TO_DOWNLOAD, ids)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = oneTimeRequestBuilder(FilesExportWork::class, JOB_IMMEDIATE_FILES_DOWNLOAD)
|
||||||
|
.setInputData(data)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
workManager.enqueueUniqueWork(JOB_IMMEDIATE_FILES_DOWNLOAD, ExistingWorkPolicy.APPEND_OR_REPLACE, request)
|
||||||
|
|
||||||
|
return workManager.getJobInfo(request.id)
|
||||||
|
}
|
||||||
|
|
||||||
override fun startImmediateContactsBackup(user: User): LiveData<JobInfo?> {
|
override fun startImmediateContactsBackup(user: User): LiveData<JobInfo?> {
|
||||||
val data = Data.Builder()
|
val data = Data.Builder()
|
||||||
.putString(ContactsBackupWork.KEY_ACCOUNT, user.accountName)
|
.putString(ContactsBackupWork.KEY_ACCOUNT, user.accountName)
|
||||||
|
|
203
app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt
Normal file
203
app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* Nextcloud Android client application
|
||||||
|
*
|
||||||
|
* @author Tobias Kaminsky
|
||||||
|
* Copyright (C) 2022 Tobias Kaminsky
|
||||||
|
* Copyright (C) 2022 Nextcloud GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.nextcloud.client.jobs
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.DownloadManager
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.nextcloud.client.account.User
|
||||||
|
import com.owncloud.android.R
|
||||||
|
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||||
|
import com.owncloud.android.datamodel.OCFile
|
||||||
|
import com.owncloud.android.files.services.FileDownloader
|
||||||
|
import com.owncloud.android.operations.DownloadType
|
||||||
|
import com.owncloud.android.ui.dialog.SendShareDialog
|
||||||
|
import com.owncloud.android.ui.notifications.NotificationUtils
|
||||||
|
import com.owncloud.android.utils.FileExportUtils
|
||||||
|
import com.owncloud.android.utils.FileStorageUtils
|
||||||
|
import com.owncloud.android.utils.theme.ThemeColorUtils
|
||||||
|
import java.security.SecureRandom
|
||||||
|
|
||||||
|
class FilesExportWork(
|
||||||
|
private val appContext: Context,
|
||||||
|
private val user: User,
|
||||||
|
private val contentResolver: ContentResolver,
|
||||||
|
private val themeColorUtils: ThemeColorUtils,
|
||||||
|
params: WorkerParameters
|
||||||
|
) : Worker(appContext, params) {
|
||||||
|
companion object {
|
||||||
|
const val FILES_TO_DOWNLOAD = "files_to_download"
|
||||||
|
private const val NUMERIC_NOTIFICATION_ID = "NUMERIC_NOTIFICATION_ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
val fileIDs = inputData.getLongArray(FILES_TO_DOWNLOAD) ?: LongArray(0)
|
||||||
|
|
||||||
|
val storageManager = FileDataStorageManager(user, contentResolver)
|
||||||
|
|
||||||
|
var successfulExports = 0
|
||||||
|
for (fileID in fileIDs) {
|
||||||
|
val ocFile = storageManager.getFileById(fileID) ?: continue
|
||||||
|
|
||||||
|
// check if storage is left
|
||||||
|
if (!FileStorageUtils.checkIfEnoughSpace(ocFile)) {
|
||||||
|
showErrorNotification(successfulExports, fileIDs.size)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ocFile.isDown) {
|
||||||
|
try {
|
||||||
|
exportFile(ocFile)
|
||||||
|
} catch (e: java.lang.RuntimeException) {
|
||||||
|
showErrorNotification(successfulExports, fileIDs.size)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
downloadFile(ocFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
successfulExports++
|
||||||
|
}
|
||||||
|
|
||||||
|
// show notification
|
||||||
|
showSuccessNotification(successfulExports)
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IllegalStateException::class)
|
||||||
|
private fun exportFile(ocFile: OCFile) {
|
||||||
|
FileExportUtils().exportFile(ocFile.fileName, ocFile.mimeType, contentResolver, ocFile, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadFile(ocFile: OCFile) {
|
||||||
|
val i = Intent(appContext, FileDownloader::class.java)
|
||||||
|
i.putExtra(FileDownloader.EXTRA_USER, user)
|
||||||
|
i.putExtra(FileDownloader.EXTRA_FILE, ocFile)
|
||||||
|
i.putExtra(SendShareDialog.PACKAGE_NAME, "")
|
||||||
|
i.putExtra(SendShareDialog.ACTIVITY_NAME, "")
|
||||||
|
i.putExtra(FileDownloader.DOWNLOAD_TYPE, DownloadType.EXPORT)
|
||||||
|
appContext.startService(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showErrorNotification(successfulExports: Int, size: Int) {
|
||||||
|
if (successfulExports == 0) {
|
||||||
|
showNotification(
|
||||||
|
appContext.getString(
|
||||||
|
R.string.export_failed,
|
||||||
|
appContext.resources.getQuantityString(R.plurals.files, size)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showNotification(
|
||||||
|
appContext.getString(
|
||||||
|
R.string.export_partially_failed,
|
||||||
|
appContext.resources.getQuantityString(R.plurals.files, successfulExports),
|
||||||
|
appContext.resources.getQuantityString(R.plurals.files, size)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSuccessNotification(successfulExports: Int) {
|
||||||
|
val files = appContext.resources.getQuantityString(R.plurals.files, successfulExports, successfulExports)
|
||||||
|
showNotification(
|
||||||
|
appContext.getString(
|
||||||
|
R.string.export_successful,
|
||||||
|
files
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNotification(message: String) {
|
||||||
|
val notificationId = SecureRandom().nextInt()
|
||||||
|
|
||||||
|
val notificationBuilder = NotificationCompat.Builder(
|
||||||
|
appContext,
|
||||||
|
NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD
|
||||||
|
)
|
||||||
|
.setSmallIcon(R.drawable.notification_icon)
|
||||||
|
.setLargeIcon(BitmapFactory.decodeResource(appContext.resources, R.drawable.notification_icon))
|
||||||
|
.setColor(themeColorUtils.primaryColor(appContext))
|
||||||
|
.setSubText(user.accountName)
|
||||||
|
.setContentText(message)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
|
||||||
|
val actionIntent = Intent(appContext, NotificationReceiver::class.java).apply {
|
||||||
|
putExtra(NUMERIC_NOTIFICATION_ID, notificationId)
|
||||||
|
}
|
||||||
|
val actionPendingIntent = PendingIntent.getBroadcast(
|
||||||
|
appContext,
|
||||||
|
notificationId,
|
||||||
|
actionIntent,
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
notificationBuilder.addAction(
|
||||||
|
NotificationCompat.Action(
|
||||||
|
null,
|
||||||
|
appContext.getString(R.string.locate_folder),
|
||||||
|
actionPendingIntent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.notify(notificationId, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
// open file chooser
|
||||||
|
val openIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS).apply {
|
||||||
|
flags = FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if intent can be resolved
|
||||||
|
if (context.packageManager.queryIntentActivities(openIntent, PackageManager.GET_RESOLVED_FILTER)
|
||||||
|
.isNotEmpty()
|
||||||
|
) {
|
||||||
|
context.startActivity(openIntent)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, R.string.open_download_folder, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove notification
|
||||||
|
val numericNotificationId = intent.getIntExtra(NUMERIC_NOTIFICATION_ID, 0)
|
||||||
|
|
||||||
|
if (numericNotificationId != 0) {
|
||||||
|
val notificationManager = context.getSystemService(Activity.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.cancel(numericNotificationId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -198,6 +198,7 @@ public class FileMenuFilter {
|
||||||
|
|
||||||
filterEdit(toShow, toHide, capability);
|
filterEdit(toShow, toHide, capability);
|
||||||
filterDownload(toShow, toHide, synchronizing);
|
filterDownload(toShow, toHide, synchronizing);
|
||||||
|
filterExport(toShow, toHide);
|
||||||
filterRename(toShow, toHide, synchronizing);
|
filterRename(toShow, toHide, synchronizing);
|
||||||
filterCopy(toShow, toHide, synchronizing);
|
filterCopy(toShow, toHide, synchronizing);
|
||||||
filterMove(toShow, toHide, synchronizing);
|
filterMove(toShow, toHide, synchronizing);
|
||||||
|
@ -460,6 +461,14 @@ public class FileMenuFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void filterExport(List<Integer> toShow, List<Integer> toHide) {
|
||||||
|
if (files.isEmpty() || containsFolder()) {
|
||||||
|
toHide.add(R.id.action_export_file);
|
||||||
|
} else {
|
||||||
|
toShow.add(R.id.action_export_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void filterStream(List<Integer> toShow, List<Integer> toHide) {
|
private void filterStream(List<Integer> toShow, List<Integer> toHide) {
|
||||||
if (files.isEmpty() || !isSingleFile() || !isSingleMedia()) {
|
if (files.isEmpty() || !isSingleFile() || !isSingleMedia()) {
|
||||||
toHide.add(R.id.action_stream_media);
|
toHide.add(R.id.action_stream_media);
|
||||||
|
|
|
@ -56,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.operations.DownloadType;
|
||||||
import com.owncloud.android.providers.DocumentsStorageProvider;
|
import com.owncloud.android.providers.DocumentsStorageProvider;
|
||||||
import com.owncloud.android.ui.activity.ConflictsResolveActivity;
|
import com.owncloud.android.ui.activity.ConflictsResolveActivity;
|
||||||
import com.owncloud.android.ui.activity.FileActivity;
|
import com.owncloud.android.ui.activity.FileActivity;
|
||||||
|
@ -96,6 +97,7 @@ public class FileDownloader extends Service
|
||||||
public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH";
|
public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH";
|
||||||
public static final String EXTRA_LINKED_TO_PATH = "LINKED_TO";
|
public static final String EXTRA_LINKED_TO_PATH = "LINKED_TO";
|
||||||
public static final String ACCOUNT_NAME = "ACCOUNT_NAME";
|
public static final String ACCOUNT_NAME = "ACCOUNT_NAME";
|
||||||
|
public static final String DOWNLOAD_TYPE = "DOWNLOAD_TYPE";
|
||||||
|
|
||||||
private static final int FOREGROUND_SERVICE_ID = 412;
|
private static final int FOREGROUND_SERVICE_ID = 412;
|
||||||
|
|
||||||
|
@ -207,6 +209,11 @@ public class FileDownloader extends Service
|
||||||
final User user = intent.getParcelableExtra(EXTRA_USER);
|
final User user = intent.getParcelableExtra(EXTRA_USER);
|
||||||
final OCFile file = intent.getParcelableExtra(EXTRA_FILE);
|
final OCFile file = intent.getParcelableExtra(EXTRA_FILE);
|
||||||
final String behaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR);
|
final String behaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR);
|
||||||
|
|
||||||
|
DownloadType downloadType = DownloadType.DOWNLOAD;
|
||||||
|
if (intent.hasExtra(DOWNLOAD_TYPE)) {
|
||||||
|
downloadType = (DownloadType) intent.getSerializableExtra(DOWNLOAD_TYPE);
|
||||||
|
}
|
||||||
String activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME);
|
String activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME);
|
||||||
String packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME);
|
String packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME);
|
||||||
conflictUploadId = intent.getLongExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, -1);
|
conflictUploadId = intent.getLongExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, -1);
|
||||||
|
@ -217,7 +224,8 @@ public class FileDownloader extends Service
|
||||||
behaviour,
|
behaviour,
|
||||||
activityName,
|
activityName,
|
||||||
packageName,
|
packageName,
|
||||||
getBaseContext());
|
getBaseContext(),
|
||||||
|
downloadType);
|
||||||
newDownload.addDatatransferProgressListener(this);
|
newDownload.addDatatransferProgressListener(this);
|
||||||
newDownload.addDatatransferProgressListener((FileDownloaderBinder) mBinder);
|
newDownload.addDatatransferProgressListener((FileDownloaderBinder) mBinder);
|
||||||
Pair<String, String> putResult = mPendingDownloads.putIfAbsent(user.getAccountName(),
|
Pair<String, String> putResult = mPendingDownloads.putIfAbsent(user.getAccountName(),
|
||||||
|
@ -381,7 +389,7 @@ public class FileDownloader extends Service
|
||||||
mBoundListeners.get(mCurrentDownload.getFile().getFileId());
|
mBoundListeners.get(mCurrentDownload.getFile().getFileId());
|
||||||
if (boundListener != null) {
|
if (boundListener != null) {
|
||||||
boundListener.onTransferProgress(progressRate, totalTransferredSoFar,
|
boundListener.onTransferProgress(progressRate, totalTransferredSoFar,
|
||||||
totalToTransfer, fileName);
|
totalToTransfer, fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -464,7 +472,7 @@ public class FileDownloader extends Service
|
||||||
|
|
||||||
/// perform the download
|
/// perform the download
|
||||||
downloadResult = mCurrentDownload.execute(mDownloadClient);
|
downloadResult = mCurrentDownload.execute(mDownloadClient);
|
||||||
if (downloadResult.isSuccess()) {
|
if (downloadResult.isSuccess() && mCurrentDownload.getDownloadType() == DownloadType.DOWNLOAD) {
|
||||||
saveDownloadedFile();
|
saveDownloadedFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
|
|
||||||
package com.owncloud.android.operations;
|
package com.owncloud.android.operations;
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
@ -38,6 +37,7 @@ 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.DownloadFileRemoteOperation;
|
import com.owncloud.android.lib.resources.files.DownloadFileRemoteOperation;
|
||||||
import com.owncloud.android.utils.EncryptionUtils;
|
import com.owncloud.android.utils.EncryptionUtils;
|
||||||
|
import com.owncloud.android.utils.FileExportUtils;
|
||||||
import com.owncloud.android.utils.FileStorageUtils;
|
import com.owncloud.android.utils.FileStorageUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -59,6 +59,7 @@ public class DownloadFileOperation extends RemoteOperation {
|
||||||
private String etag = "";
|
private String etag = "";
|
||||||
private String activityName;
|
private String activityName;
|
||||||
private String packageName;
|
private String packageName;
|
||||||
|
private DownloadType downloadType;
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
private Set<OnDatatransferProgressListener> dataTransferListeners = new HashSet<>();
|
private Set<OnDatatransferProgressListener> dataTransferListeners = new HashSet<>();
|
||||||
|
@ -67,15 +68,20 @@ public class DownloadFileOperation extends RemoteOperation {
|
||||||
|
|
||||||
private final AtomicBoolean cancellationRequested = new AtomicBoolean(false);
|
private final AtomicBoolean cancellationRequested = new AtomicBoolean(false);
|
||||||
|
|
||||||
public DownloadFileOperation(User user, OCFile file, String behaviour, String activityName,
|
public DownloadFileOperation(User user,
|
||||||
String packageName, Context context) {
|
OCFile file,
|
||||||
|
String behaviour,
|
||||||
|
String activityName,
|
||||||
|
String packageName,
|
||||||
|
Context context,
|
||||||
|
DownloadType downloadType) {
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new IllegalArgumentException("Illegal null user in DownloadFileOperation " +
|
throw new IllegalArgumentException("Illegal null user in DownloadFileOperation " +
|
||||||
"creation");
|
"creation");
|
||||||
}
|
}
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
throw new IllegalArgumentException("Illegal null file in DownloadFileOperation " +
|
throw new IllegalArgumentException("Illegal null file in DownloadFileOperation " +
|
||||||
"creation");
|
"creation");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.user = user;
|
this.user = user;
|
||||||
|
@ -84,10 +90,11 @@ public class DownloadFileOperation extends RemoteOperation {
|
||||||
this.activityName = activityName;
|
this.activityName = activityName;
|
||||||
this.packageName = packageName;
|
this.packageName = packageName;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
this.downloadType = downloadType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadFileOperation(User user, OCFile file, Context context) {
|
public DownloadFileOperation(User user, OCFile file, Context context) {
|
||||||
this(user, file, null, null, null, context);
|
this(user, file, null, null, null, context, DownloadType.DOWNLOAD);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSavePath() {
|
public String getSavePath() {
|
||||||
|
@ -153,27 +160,35 @@ public class DownloadFileOperation extends RemoteOperation {
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteOperationResult result;
|
RemoteOperationResult result;
|
||||||
File newFile;
|
File newFile = null;
|
||||||
boolean moved;
|
boolean moved;
|
||||||
|
|
||||||
/// download will be performed to a temporal file, then moved to the final location
|
/// download will be performed to a temporal file, then moved to the final location
|
||||||
File tmpFile = new File(getTmpPath());
|
File tmpFile = new File(getTmpPath());
|
||||||
|
|
||||||
String tmpFolder = getTmpFolder();
|
String tmpFolder = getTmpFolder();
|
||||||
|
|
||||||
downloadOperation = new DownloadFileRemoteOperation(file.getRemotePath(), tmpFolder);
|
downloadOperation = new DownloadFileRemoteOperation(file.getRemotePath(), tmpFolder);
|
||||||
Iterator<OnDatatransferProgressListener> listener = dataTransferListeners.iterator();
|
|
||||||
while (listener.hasNext()) {
|
if (downloadType == DownloadType.DOWNLOAD) {
|
||||||
downloadOperation.addDatatransferProgressListener(listener.next());
|
Iterator<OnDatatransferProgressListener> listener = dataTransferListeners.iterator();
|
||||||
|
while (listener.hasNext()) {
|
||||||
|
downloadOperation.addDatatransferProgressListener(listener.next());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result = downloadOperation.execute(client);
|
result = downloadOperation.execute(client);
|
||||||
|
|
||||||
if (result.isSuccess()) {
|
if (result.isSuccess()) {
|
||||||
modificationTimestamp = downloadOperation.getModificationTimestamp();
|
modificationTimestamp = downloadOperation.getModificationTimestamp();
|
||||||
etag = downloadOperation.getEtag();
|
etag = downloadOperation.getEtag();
|
||||||
newFile = new File(getSavePath());
|
|
||||||
if (!newFile.getParentFile().exists() && !newFile.getParentFile().mkdirs()) {
|
if (downloadType == DownloadType.DOWNLOAD) {
|
||||||
Log_OC.e(TAG, "Unable to create parent folder " + newFile.getParentFile().getAbsolutePath());
|
newFile = new File(getSavePath());
|
||||||
|
|
||||||
|
if (!newFile.getParentFile().exists() && !newFile.getParentFile().mkdirs()) {
|
||||||
|
Log_OC.e(TAG, "Unable to create parent folder " + newFile.getParentFile().getAbsolutePath());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decrypt file
|
// decrypt file
|
||||||
|
@ -207,10 +222,22 @@ public class DownloadFileOperation extends RemoteOperation {
|
||||||
return new RemoteOperationResult(e);
|
return new RemoteOperationResult(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
moved = tmpFile.renameTo(newFile);
|
|
||||||
newFile.setLastModified(file.getModificationTimestamp());
|
if (downloadType == DownloadType.DOWNLOAD) {
|
||||||
if (!moved) {
|
moved = tmpFile.renameTo(newFile);
|
||||||
result = new RemoteOperationResult(RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_MOVED);
|
newFile.setLastModified(file.getModificationTimestamp());
|
||||||
|
if (!moved) {
|
||||||
|
result = new RemoteOperationResult(RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_MOVED);
|
||||||
|
}
|
||||||
|
} else if (downloadType == DownloadType.EXPORT) {
|
||||||
|
new FileExportUtils().exportFile(file.getFileName(),
|
||||||
|
file.getMimeType(),
|
||||||
|
context.getContentResolver(),
|
||||||
|
null,
|
||||||
|
tmpFile);
|
||||||
|
if (!tmpFile.delete()) {
|
||||||
|
Log_OC.e(TAG, "Deletion of " + tmpFile.getAbsolutePath() + " failed!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log_OC.i(TAG, "Download of " + file.getRemotePath() + " to " + getSavePath() + ": " +
|
Log_OC.i(TAG, "Download of " + file.getRemotePath() + " to " + getSavePath() + ": " +
|
||||||
|
@ -262,4 +289,8 @@ public class DownloadFileOperation extends RemoteOperation {
|
||||||
public String getPackageName() {
|
public String getPackageName() {
|
||||||
return this.packageName;
|
return this.packageName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DownloadType getDownloadType() {
|
||||||
|
return downloadType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* Nextcloud Android client application
|
||||||
|
*
|
||||||
|
* @author Tobias Kaminsky
|
||||||
|
* Copyright (C) 2022 Tobias Kaminsky
|
||||||
|
* Copyright (C) 2022 Nextcloud GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.owncloud.android.operations
|
||||||
|
|
||||||
|
enum class DownloadType {
|
||||||
|
DOWNLOAD,
|
||||||
|
EXPORT
|
||||||
|
}
|
|
@ -21,7 +21,6 @@
|
||||||
|
|
||||||
package com.owncloud.android.operations;
|
package com.owncloud.android.operations;
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
|
@ -55,6 +55,7 @@ import com.nextcloud.client.account.User;
|
||||||
import com.nextcloud.client.account.UserAccountManager;
|
import com.nextcloud.client.account.UserAccountManager;
|
||||||
import com.nextcloud.client.device.DeviceInfo;
|
import com.nextcloud.client.device.DeviceInfo;
|
||||||
import com.nextcloud.client.di.Injectable;
|
import com.nextcloud.client.di.Injectable;
|
||||||
|
import com.nextcloud.client.jobs.BackgroundJobManager;
|
||||||
import com.nextcloud.client.network.ClientFactory;
|
import com.nextcloud.client.network.ClientFactory;
|
||||||
import com.nextcloud.client.preferences.AppPreferences;
|
import com.nextcloud.client.preferences.AppPreferences;
|
||||||
import com.nextcloud.client.utils.Throttler;
|
import com.nextcloud.client.utils.Throttler;
|
||||||
|
@ -133,7 +134,6 @@ import javax.inject.Inject;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
|
@ -175,6 +175,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
||||||
public static final String DOWNLOAD_BEHAVIOUR = "DOWNLOAD_BEHAVIOUR";
|
public static final String DOWNLOAD_BEHAVIOUR = "DOWNLOAD_BEHAVIOUR";
|
||||||
public static final String DOWNLOAD_SEND = "DOWNLOAD_SEND";
|
public static final String DOWNLOAD_SEND = "DOWNLOAD_SEND";
|
||||||
|
|
||||||
|
|
||||||
public static final String FOLDER_LAYOUT_LIST = "LIST";
|
public static final String FOLDER_LAYOUT_LIST = "LIST";
|
||||||
public static final String FOLDER_LAYOUT_GRID = "GRID";
|
public static final String FOLDER_LAYOUT_GRID = "GRID";
|
||||||
|
|
||||||
|
@ -187,7 +188,6 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
||||||
private static final String DIALOG_CREATE_FOLDER = "DIALOG_CREATE_FOLDER";
|
private static final String DIALOG_CREATE_FOLDER = "DIALOG_CREATE_FOLDER";
|
||||||
private static final String DIALOG_CREATE_DOCUMENT = "DIALOG_CREATE_DOCUMENT";
|
private static final String DIALOG_CREATE_DOCUMENT = "DIALOG_CREATE_DOCUMENT";
|
||||||
private static final String DIALOG_BOTTOM_SHEET = "DIALOG_BOTTOM_SHEET";
|
private static final String DIALOG_BOTTOM_SHEET = "DIALOG_BOTTOM_SHEET";
|
||||||
private static final String DIALOG_LOCK_DETAILS = "DIALOG_LOCK_DETAILS";
|
|
||||||
|
|
||||||
private static final int SINGLE_SELECTION = 1;
|
private static final int SINGLE_SELECTION = 1;
|
||||||
private static final int NOT_ENOUGH_SPACE_FRAG_REQUEST_CODE = 2;
|
private static final int NOT_ENOUGH_SPACE_FRAG_REQUEST_CODE = 2;
|
||||||
|
@ -202,6 +202,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
||||||
@Inject ThemeUtils themeUtils;
|
@Inject ThemeUtils themeUtils;
|
||||||
@Inject ThemeAvatarUtils themeAvatarUtils;
|
@Inject ThemeAvatarUtils themeAvatarUtils;
|
||||||
@Inject ArbitraryDataProvider arbitraryDataProvider;
|
@Inject ArbitraryDataProvider arbitraryDataProvider;
|
||||||
|
@Inject BackgroundJobManager backgroundJobManager;
|
||||||
|
|
||||||
protected FileFragment.ContainerActivity mContainerActivity;
|
protected FileFragment.ContainerActivity mContainerActivity;
|
||||||
|
|
||||||
|
@ -222,7 +223,6 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
||||||
protected String mLimitToMimeType;
|
protected String mLimitToMimeType;
|
||||||
private FloatingActionButton mFabMain;
|
private FloatingActionButton mFabMain;
|
||||||
|
|
||||||
|
|
||||||
@Inject DeviceInfo deviceInfo;
|
@Inject DeviceInfo deviceInfo;
|
||||||
|
|
||||||
protected enum MenuItemAddRemove {
|
protected enum MenuItemAddRemove {
|
||||||
|
@ -1089,7 +1089,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
if (requestCode == SetupEncryptionDialogFragment.SETUP_ENCRYPTION_REQUEST_CODE &&
|
if (requestCode == SetupEncryptionDialogFragment.SETUP_ENCRYPTION_REQUEST_CODE &&
|
||||||
resultCode == SetupEncryptionDialogFragment.SETUP_ENCRYPTION_RESULT_CODE &&
|
resultCode == SetupEncryptionDialogFragment.SETUP_ENCRYPTION_RESULT_CODE &&
|
||||||
data.getBooleanExtra(SetupEncryptionDialogFragment.SUCCESS, false)) {
|
data.getBooleanExtra(SetupEncryptionDialogFragment.SUCCESS, false)) {
|
||||||
|
|
||||||
int position = data.getIntExtra(SetupEncryptionDialogFragment.ARG_POSITION, -1);
|
int position = data.getIntExtra(SetupEncryptionDialogFragment.ARG_POSITION, -1);
|
||||||
OCFile file = mAdapter.getItem(position);
|
OCFile file = mAdapter.getItem(position);
|
||||||
|
@ -1186,6 +1186,10 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
||||||
syncAndCheckFiles(checkedFiles);
|
syncAndCheckFiles(checkedFiles);
|
||||||
exitSelectionMode();
|
exitSelectionMode();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (itemId == R.id.action_export_file) {
|
||||||
|
exportFiles(checkedFiles);
|
||||||
|
exitSelectionMode();
|
||||||
|
return true;
|
||||||
} else if (itemId == R.id.action_cancel_sync) {
|
} else if (itemId == R.id.action_cancel_sync) {
|
||||||
((FileDisplayActivity) mContainerActivity).cancelTransference(checkedFiles);
|
((FileDisplayActivity) mContainerActivity).cancelTransference(checkedFiles);
|
||||||
return true;
|
return true;
|
||||||
|
@ -1818,13 +1822,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
||||||
// Get the remaining space on device
|
// Get the remaining space on device
|
||||||
long availableSpaceOnDevice = FileOperationsHelper.getAvailableSpaceOnDevice();
|
long availableSpaceOnDevice = FileOperationsHelper.getAvailableSpaceOnDevice();
|
||||||
|
|
||||||
// Determine if space is enough to download the file, -1 available space if there in error while computing
|
if (FileStorageUtils.checkIfEnoughSpace(file)) {
|
||||||
boolean isSpaceEnough = true;
|
|
||||||
if (availableSpaceOnDevice >= 0) {
|
|
||||||
isSpaceEnough = checkIfEnoughSpace(availableSpaceOnDevice, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSpaceEnough) {
|
|
||||||
mContainerActivity.getFileOperationsHelper().syncFile(file);
|
mContainerActivity.getFileOperationsHelper().syncFile(file);
|
||||||
} else {
|
} else {
|
||||||
showSpaceErrorDialog(file, availableSpaceOnDevice);
|
showSpaceErrorDialog(file, availableSpaceOnDevice);
|
||||||
|
@ -1832,24 +1830,21 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
private void exportFiles(Collection<OCFile> files) {
|
||||||
public boolean checkIfEnoughSpace(long availableSpaceOnDevice, OCFile file) {
|
Context context = getContext();
|
||||||
if (file.isFolder()) {
|
View view = getView();
|
||||||
// on folders we assume that we only need difference
|
|
||||||
return availableSpaceOnDevice > (file.getFileLength() - localFolderSize(file));
|
|
||||||
} else {
|
|
||||||
// on files complete file must first be stored, then target gets overwritten
|
|
||||||
return availableSpaceOnDevice > file.getFileLength();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private long localFolderSize(OCFile file) {
|
if (context != null && view != null) {
|
||||||
if (file.getStoragePath() == null) {
|
DisplayUtils.showSnackMessage(view,
|
||||||
// not yet downloaded anything
|
context.getString(
|
||||||
return 0;
|
R.string.export_start,
|
||||||
} else {
|
context.getResources().getQuantityString(R.plurals.files,
|
||||||
return FileStorageUtils.getFolderSize(new File(file.getStoragePath()));
|
files.size(),
|
||||||
|
files.size())
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
backgroundJobManager.startImmediateFilesDownloadJob(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showSpaceErrorDialog(OCFile file, long availableSpaceOnDevice) {
|
private void showSpaceErrorDialog(OCFile file, long availableSpaceOnDevice) {
|
||||||
|
|
|
@ -880,7 +880,7 @@ public class FileOperationsHelper {
|
||||||
intent.putExtra(OperationsService.EXTRA_SYNC_FILE_CONTENTS, true);
|
intent.putExtra(OperationsService.EXTRA_SYNC_FILE_CONTENTS, true);
|
||||||
mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(intent);
|
mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(intent);
|
||||||
fileActivity.showLoadingDialog(fileActivity.getApplicationContext().
|
fileActivity.showLoadingDialog(fileActivity.getApplicationContext().
|
||||||
getString(R.string.wait_a_moment));
|
getString(R.string.wait_a_moment));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Intent intent = new Intent(fileActivity, OperationsService.class);
|
Intent intent = new Intent(fileActivity, OperationsService.class);
|
||||||
|
|
181
app/src/main/java/com/owncloud/android/utils/FileExportUtils.kt
Normal file
181
app/src/main/java/com/owncloud/android/utils/FileExportUtils.kt
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* Nextcloud Android client application
|
||||||
|
*
|
||||||
|
* @author Tobias Kaminsky
|
||||||
|
* Copyright (C) 2022 Tobias Kaminsky
|
||||||
|
* Copyright (C) 2022 Nextcloud GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.owncloud.android.utils
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import com.owncloud.android.datamodel.OCFile
|
||||||
|
import com.owncloud.android.lib.common.utils.Log_OC
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class FileExportUtils {
|
||||||
|
companion object {
|
||||||
|
const val INITIAL_RENAME_COUNT = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IllegalStateException::class)
|
||||||
|
fun exportFile(
|
||||||
|
fileName: String,
|
||||||
|
mimeType: String,
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
ocFile: OCFile?,
|
||||||
|
file: File?
|
||||||
|
) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
exportFileAndroid10AndAbove(
|
||||||
|
fileName,
|
||||||
|
mimeType,
|
||||||
|
contentResolver,
|
||||||
|
ocFile,
|
||||||
|
file
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
exportFilesBelowAndroid10(
|
||||||
|
fileName,
|
||||||
|
contentResolver,
|
||||||
|
ocFile,
|
||||||
|
file
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
private fun exportFileAndroid10AndAbove(
|
||||||
|
fileName: String,
|
||||||
|
mimeType: String,
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
ocFile: OCFile?,
|
||||||
|
file: File?
|
||||||
|
) {
|
||||||
|
val cv = ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
}
|
||||||
|
|
||||||
|
var uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, cv)
|
||||||
|
|
||||||
|
if (uri == null) {
|
||||||
|
var count = INITIAL_RENAME_COUNT
|
||||||
|
do {
|
||||||
|
val name = generateNewName(fileName, count)
|
||||||
|
cv.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
|
||||||
|
uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, cv)
|
||||||
|
|
||||||
|
count++
|
||||||
|
} while (uri == null)
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(
|
||||||
|
ocFile,
|
||||||
|
file,
|
||||||
|
contentResolver,
|
||||||
|
FileOutputStream(contentResolver.openFileDescriptor(uri, "w")?.fileDescriptor)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exportFilesBelowAndroid10(
|
||||||
|
fileName: String,
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
ocFile: OCFile?,
|
||||||
|
file: File?
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
var target = File(
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
fileName
|
||||||
|
)
|
||||||
|
|
||||||
|
if (target.exists()) {
|
||||||
|
var count = INITIAL_RENAME_COUNT
|
||||||
|
do {
|
||||||
|
val name = generateNewName(fileName, count)
|
||||||
|
target = File(
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
name
|
||||||
|
)
|
||||||
|
|
||||||
|
count++
|
||||||
|
} while (target.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(
|
||||||
|
ocFile,
|
||||||
|
file,
|
||||||
|
contentResolver,
|
||||||
|
FileOutputStream(target)
|
||||||
|
)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
Log_OC.e(this, "File not found", e)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log_OC.e(this, "Cannot write file", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IllegalStateException::class)
|
||||||
|
private fun copy(ocFile: OCFile?, file: File?, contentResolver: ContentResolver, outputStream: FileOutputStream) {
|
||||||
|
try {
|
||||||
|
val inputStream = if (ocFile != null) {
|
||||||
|
contentResolver.openInputStream(ocFile.storageUri)
|
||||||
|
} else if (file != null) {
|
||||||
|
FileInputStream(file)
|
||||||
|
} else {
|
||||||
|
throw IllegalStateException("ocFile and file both may not be null")
|
||||||
|
}
|
||||||
|
|
||||||
|
inputStream.use { fis ->
|
||||||
|
outputStream.use { os ->
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
var len: Int
|
||||||
|
while (fis!!.read(buffer).also { len = it } != -1) {
|
||||||
|
os.write(buffer, 0, len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log_OC.e(this, "Cannot write file", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateNewName(name: String, count: Int): String {
|
||||||
|
val extPos = name.lastIndexOf('.')
|
||||||
|
val suffix = " ($count)"
|
||||||
|
|
||||||
|
return if (extPos >= 0) {
|
||||||
|
val extension = name.substring(extPos + 1)
|
||||||
|
val nameWithoutExtension = name.substring(0, extPos)
|
||||||
|
|
||||||
|
"$nameWithoutExtension$suffix.$extension"
|
||||||
|
} else {
|
||||||
|
name + suffix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,6 +35,7 @@ import com.owncloud.android.datamodel.FileDataStorageManager;
|
||||||
import com.owncloud.android.datamodel.OCFile;
|
import com.owncloud.android.datamodel.OCFile;
|
||||||
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.model.RemoteFile;
|
import com.owncloud.android.lib.resources.files.model.RemoteFile;
|
||||||
|
import com.owncloud.android.ui.helpers.FileOperationsHelper;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
@ -56,6 +57,7 @@ import java.util.TimeZone;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
import androidx.core.app.ActivityCompat;
|
import androidx.core.app.ActivityCompat;
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
|
|
||||||
|
@ -639,6 +641,44 @@ public final class FileStorageUtils {
|
||||||
return f.canRead() && f.isDirectory();
|
return f.canRead() && f.isDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* // Determine if space is enough to download the file
|
||||||
|
*
|
||||||
|
* @param file @link{OCFile}
|
||||||
|
* @return boolean: true if there is enough space left
|
||||||
|
* @throws RuntimeException
|
||||||
|
*/
|
||||||
|
public static boolean checkIfEnoughSpace(OCFile file) {
|
||||||
|
// Get the remaining space on device
|
||||||
|
long availableSpaceOnDevice = FileOperationsHelper.getAvailableSpaceOnDevice();
|
||||||
|
|
||||||
|
if (availableSpaceOnDevice == -1) {
|
||||||
|
throw new RuntimeException("Error while computing available space");
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkIfEnoughSpace(availableSpaceOnDevice, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public static boolean checkIfEnoughSpace(long availableSpaceOnDevice, OCFile file) {
|
||||||
|
if (file.isFolder()) {
|
||||||
|
// on folders we assume that we only need difference
|
||||||
|
return availableSpaceOnDevice > (file.getFileLength() - localFolderSize(file));
|
||||||
|
} else {
|
||||||
|
// on files complete file must first be stored, then target gets overwritten
|
||||||
|
return availableSpaceOnDevice > file.getFileLength();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long localFolderSize(OCFile file) {
|
||||||
|
if (file.getStoragePath() == null) {
|
||||||
|
// not yet downloaded anything
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return FileStorageUtils.getFolderSize(new File(file.getStoragePath()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should be converted to an enum when we only support min SDK version for Environment.DIRECTORY_DOCUMENTS
|
* Should be converted to an enum when we only support min SDK version for Environment.DIRECTORY_DOCUMENTS
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -107,6 +107,12 @@
|
||||||
app:showAsAction="never"
|
app:showAsAction="never"
|
||||||
android:showAsAction="never" />
|
android:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_export_file"
|
||||||
|
android:title="@string/filedetails_export"
|
||||||
|
app:showAsAction="never"
|
||||||
|
android:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_stream_media"
|
android:id="@+id/action_stream_media"
|
||||||
android:title="@string/stream"
|
android:title="@string/stream"
|
||||||
|
|
|
@ -200,6 +200,10 @@
|
||||||
<item quantity="one">Found %d duplicate entry.</item>
|
<item quantity="one">Found %d duplicate entry.</item>
|
||||||
<item quantity="other">Found %d duplicate entries.</item>
|
<item quantity="other">Found %d duplicate entries.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="files">
|
||||||
|
<item quantity="one">%d file</item>
|
||||||
|
<item quantity="other">%d files</item>
|
||||||
|
</plurals>
|
||||||
<string name="sync_foreign_files_forgotten_explanation">As of version 1.3.16, files uploaded from this device are copied into the local %1$s folder to prevent data loss when a single file is synced with multiple accounts.\n\nDue to this change, all files uploaded with earlier versions of this app were copied into the %2$s folder. However, an error prevented the completion of this operation during account synchronization. You may either leave the file(s) as is and delete the link to %3$s, or move the file(s) into the %1$s folder and retain the link to %4$s.\n\nListed below are the local file(s), and the remote file(s) in %5$s they were linked to.</string>
|
<string name="sync_foreign_files_forgotten_explanation">As of version 1.3.16, files uploaded from this device are copied into the local %1$s folder to prevent data loss when a single file is synced with multiple accounts.\n\nDue to this change, all files uploaded with earlier versions of this app were copied into the %2$s folder. However, an error prevented the completion of this operation during account synchronization. You may either leave the file(s) as is and delete the link to %3$s, or move the file(s) into the %1$s folder and retain the link to %4$s.\n\nListed below are the local file(s), and the remote file(s) in %5$s they were linked to.</string>
|
||||||
<string name="sync_current_folder_was_removed">The folder %1$s does not exist anymore</string>
|
<string name="sync_current_folder_was_removed">The folder %1$s does not exist anymore</string>
|
||||||
<string name="foreign_files_move">Move all</string>
|
<string name="foreign_files_move">Move all</string>
|
||||||
|
@ -1014,4 +1018,11 @@
|
||||||
<string name="locked_by">Locked by %1$s</string>
|
<string name="locked_by">Locked by %1$s</string>
|
||||||
<string name="locked_by_app">Locked by %1$s app</string>
|
<string name="locked_by_app">Locked by %1$s app</string>
|
||||||
<string name="lock_expiration_info">Expires: %1$s</string>
|
<string name="lock_expiration_info">Expires: %1$s</string>
|
||||||
|
<string name="filedetails_export">Export</string>
|
||||||
|
<string name="locate_folder">Locate folder</string>
|
||||||
|
<string name="export_successful">Exported %1s</string>
|
||||||
|
<string name="export_failed">Failed to export %1s</string>
|
||||||
|
<string name="export_partially_failed">Exported %1s, skipped %2s due to error</string>
|
||||||
|
<string name="export_start">%1s will be exported. See notification for details.</string>
|
||||||
|
<string name="open_download_folder">Cannot open folder. Please manually browse to Download folder</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue