mirror of
https://github.com/nextcloud/android.git
synced 2024-12-18 23:11:58 +03:00
Merge remote-tracking branch 'origin/master' into dev
This commit is contained in:
commit
d94dbe85fc
48 changed files with 2132 additions and 323 deletions
|
@ -319,7 +319,7 @@ dependencies {
|
|||
qaImplementation project(':appscan')
|
||||
|
||||
spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0'
|
||||
spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.4'
|
||||
spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.5'
|
||||
|
||||
implementation "com.google.dagger:dagger:$daggerVersion"
|
||||
implementation "com.google.dagger:dagger-android:$daggerVersion"
|
||||
|
|
1301
app/schemas/com.nextcloud.client.database.NextcloudDatabase/85.json
Normal file
1301
app/schemas/com.nextcloud.client.database.NextcloudDatabase/85.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -377,8 +377,8 @@ public abstract class AbstractIT {
|
|||
public void uploadOCUpload(OCUpload ocUpload) {
|
||||
ConnectivityService connectivityServiceMock = new ConnectivityService() {
|
||||
@Override
|
||||
public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
|
||||
return false;
|
||||
public void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -189,8 +189,8 @@ public abstract class AbstractOnServerIT extends AbstractIT {
|
|||
public void uploadOCUpload(OCUpload ocUpload, int localBehaviour) {
|
||||
ConnectivityService connectivityServiceMock = new ConnectivityService() {
|
||||
@Override
|
||||
public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
|
||||
return false;
|
||||
public void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -59,8 +59,8 @@ public class UploadIT extends AbstractOnServerIT {
|
|||
|
||||
private ConnectivityService connectivityServiceMock = new ConnectivityService() {
|
||||
@Override
|
||||
public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
|
||||
return false;
|
||||
public void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -282,8 +282,8 @@ public class UploadIT extends AbstractOnServerIT {
|
|||
public void testUploadOnWifiOnlyButNoWifi() {
|
||||
ConnectivityService connectivityServiceMock = new ConnectivityService() {
|
||||
@Override
|
||||
public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
|
||||
return false;
|
||||
public void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -371,8 +371,8 @@ public class UploadIT extends AbstractOnServerIT {
|
|||
public void testUploadOnWifiOnlyButMeteredWifi() {
|
||||
ConnectivityService connectivityServiceMock = new ConnectivityService() {
|
||||
@Override
|
||||
public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
|
||||
return false;
|
||||
public void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -34,9 +34,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
|
|||
private var uploadsStorageManager: UploadsStorageManager? = null
|
||||
|
||||
private val connectivityServiceMock: ConnectivityService = object : ConnectivityService {
|
||||
override fun isNetworkAndServerAvailable(): Boolean {
|
||||
return false
|
||||
}
|
||||
override fun isNetworkAndServerAvailable(callback: ConnectivityService.GenericCallback<Boolean>) = Unit
|
||||
|
||||
override fun isConnected(): Boolean {
|
||||
return false
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.owncloud.android.ui.dialog;
|
||||
|
||||
import com.owncloud.android.AbstractIT;
|
||||
import com.owncloud.android.datamodel.OCFile;
|
||||
import com.owncloud.android.ui.activity.FileDisplayActivity;
|
||||
import com.owncloud.android.utils.ScreenshotTest;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.test.espresso.intent.rule.IntentsTestRule;
|
||||
|
||||
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
public class SyncFileNotEnoughSpaceDialogFragmentTest extends AbstractIT {
|
||||
@Rule public IntentsTestRule<FileDisplayActivity> activityRule = new IntentsTestRule<>(FileDisplayActivity.class,
|
||||
true,
|
||||
false);
|
||||
|
||||
@Test
|
||||
@ScreenshotTest
|
||||
public void showNotEnoughSpaceDialogForFolder() {
|
||||
FileDisplayActivity test = activityRule.launchActivity(null);
|
||||
OCFile ocFile = new OCFile("/Document/");
|
||||
ocFile.setFileLength(5000000);
|
||||
ocFile.setFolder();
|
||||
|
||||
SyncFileNotEnoughSpaceDialogFragment dialog = SyncFileNotEnoughSpaceDialogFragment.newInstance(ocFile, 1000);
|
||||
dialog.show(test.getListOfFilesFragment().getFragmentManager(), "1");
|
||||
|
||||
getInstrumentation().waitForIdleSync();
|
||||
|
||||
screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ScreenshotTest
|
||||
public void showNotEnoughSpaceDialogForFile() {
|
||||
FileDisplayActivity test = activityRule.launchActivity(null);
|
||||
OCFile ocFile = new OCFile("/Video.mp4");
|
||||
ocFile.setFileLength(1000000);
|
||||
|
||||
SyncFileNotEnoughSpaceDialogFragment dialog = SyncFileNotEnoughSpaceDialogFragment.newInstance(ocFile, 2000);
|
||||
dialog.show(test.getListOfFilesFragment().getFragmentManager(), "2");
|
||||
|
||||
getInstrumentation().waitForIdleSync();
|
||||
|
||||
screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.owncloud.android.ui.dialog
|
||||
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.test.core.app.launchActivity
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.IdlingRegistry
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isRoot
|
||||
import com.owncloud.android.AbstractIT
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.ui.activity.FileDisplayActivity
|
||||
import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment.Companion.newInstance
|
||||
import com.owncloud.android.utils.EspressoIdlingResource
|
||||
import com.owncloud.android.utils.ScreenshotTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class SyncFileNotEnoughSpaceDialogFragmentTest : AbstractIT() {
|
||||
private val testClassName = "com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragmentTest"
|
||||
|
||||
@Before
|
||||
fun registerIdlingResource() {
|
||||
IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource)
|
||||
}
|
||||
|
||||
@After
|
||||
fun unregisterIdlingResource() {
|
||||
IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ScreenshotTest
|
||||
@UiThread
|
||||
fun showNotEnoughSpaceDialogForFolder() {
|
||||
launchActivity<FileDisplayActivity>().use { scenario ->
|
||||
scenario.onActivity { sut ->
|
||||
val ocFile = OCFile("/Document/").apply {
|
||||
fileLength = 5000000
|
||||
setFolder()
|
||||
}
|
||||
|
||||
onIdleSync {
|
||||
EspressoIdlingResource.increment()
|
||||
newInstance(ocFile, 1000).apply {
|
||||
show(sut.supportFragmentManager, "1")
|
||||
}
|
||||
EspressoIdlingResource.decrement()
|
||||
|
||||
val screenShotName = createName(testClassName + "_" + "showNotEnoughSpaceDialogForFolder", "")
|
||||
onView(isRoot()).check(matches(isDisplayed()))
|
||||
screenshotViaName(sut, screenShotName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ScreenshotTest
|
||||
@UiThread
|
||||
fun showNotEnoughSpaceDialogForFile() {
|
||||
launchActivity<FileDisplayActivity>().use { scenario ->
|
||||
scenario.onActivity { sut ->
|
||||
val ocFile = OCFile("/Video.mp4").apply {
|
||||
fileLength = 1000000
|
||||
}
|
||||
|
||||
onIdleSync {
|
||||
EspressoIdlingResource.increment()
|
||||
newInstance(ocFile, 2000).apply {
|
||||
show(sut.supportFragmentManager, "2")
|
||||
}
|
||||
EspressoIdlingResource.decrement()
|
||||
|
||||
val screenShotName = createName(testClassName + "_" + "showNotEnoughSpaceDialogForFile", "")
|
||||
onView(isRoot()).check(matches(isDisplayed()))
|
||||
screenshotViaName(sut, screenShotName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,6 +42,8 @@ class TestActivity :
|
|||
private lateinit var binding: TestLayoutBinding
|
||||
|
||||
val connectivityServiceMock: ConnectivityService = object : ConnectivityService {
|
||||
override fun isNetworkAndServerAvailable(callback: ConnectivityService.GenericCallback<Boolean>) = Unit
|
||||
|
||||
override fun isConnected(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
@ -53,10 +55,6 @@ class TestActivity :
|
|||
override fun getConnectivity(): Connectivity {
|
||||
return Connectivity.CONNECTED_WIFI
|
||||
}
|
||||
|
||||
override fun isNetworkAndServerAvailable(): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import androidx.room.AutoMigration
|
|||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.nextcloud.client.core.Clock
|
||||
import com.nextcloud.client.core.ClockImpl
|
||||
import com.nextcloud.client.database.dao.ArbitraryDataDao
|
||||
|
@ -31,6 +32,7 @@ import com.nextcloud.client.database.migrations.DatabaseMigrationUtil
|
|||
import com.nextcloud.client.database.migrations.Migration67to68
|
||||
import com.nextcloud.client.database.migrations.RoomMigration
|
||||
import com.nextcloud.client.database.migrations.addLegacyMigrations
|
||||
import com.nextcloud.client.database.typeConverter.OfflineOperationTypeConverter
|
||||
import com.owncloud.android.db.ProviderMeta
|
||||
|
||||
@Database(
|
||||
|
@ -65,11 +67,13 @@ import com.owncloud.android.db.ProviderMeta
|
|||
AutoMigration(from = 80, to = 81),
|
||||
AutoMigration(from = 81, to = 82),
|
||||
AutoMigration(from = 82, to = 83),
|
||||
AutoMigration(from = 83, to = 84)
|
||||
AutoMigration(from = 83, to = 84),
|
||||
AutoMigration(from = 84, to = 85, spec = DatabaseMigrationUtil.DeleteColumnSpec::class)
|
||||
],
|
||||
exportSchema = true
|
||||
)
|
||||
@Suppress("Detekt.UnnecessaryAbstractClass") // needed by Room
|
||||
@TypeConverters(OfflineOperationTypeConverter::class)
|
||||
abstract class NextcloudDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun arbitraryDataDao(): ArbitraryDataDao
|
||||
|
@ -93,6 +97,7 @@ abstract class NextcloudDatabase : RoomDatabase() {
|
|||
instance = Room
|
||||
.databaseBuilder(context, NextcloudDatabase::class.java, ProviderMeta.DB_NAME)
|
||||
.allowMainThreadQueries()
|
||||
.addTypeConverter(OfflineOperationTypeConverter())
|
||||
.addLegacyMigrations(clock, context)
|
||||
.addMigrations(RoomMigration())
|
||||
.addMigrations(Migration67to68())
|
||||
|
|
|
@ -10,6 +10,7 @@ package com.nextcloud.client.database.dao
|
|||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
|
@ -19,7 +20,7 @@ interface OfflineOperationDao {
|
|||
@Query("SELECT * FROM offline_operations")
|
||||
fun getAll(): List<OfflineOperationEntity>
|
||||
|
||||
@Insert
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg entity: OfflineOperationEntity)
|
||||
|
||||
@Update
|
||||
|
@ -35,5 +36,8 @@ interface OfflineOperationDao {
|
|||
fun getByPath(path: String): OfflineOperationEntity?
|
||||
|
||||
@Query("SELECT * FROM offline_operations WHERE offline_operations_parent_oc_file_id = :parentOCFileId")
|
||||
fun getSubDirectoriesByParentOCFileId(parentOCFileId: Long): List<OfflineOperationEntity>
|
||||
fun getSubEntitiesByParentOCFileId(parentOCFileId: Long): List<OfflineOperationEntity>
|
||||
|
||||
@Query("DELETE FROM offline_operations")
|
||||
fun clearTable()
|
||||
}
|
||||
|
|
|
@ -22,18 +22,18 @@ data class OfflineOperationEntity(
|
|||
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PARENT_OC_FILE_ID)
|
||||
var parentOCFileId: Long? = null,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PARENT_PATH)
|
||||
var parentPath: String? = null,
|
||||
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PATH)
|
||||
var path: String? = null,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_TYPE)
|
||||
var type: OfflineOperationType? = null,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PATH)
|
||||
var path: String? = null,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_FILE_NAME)
|
||||
var filename: String? = null,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_CREATED_AT)
|
||||
var createdAt: Long? = null
|
||||
var createdAt: Long? = null,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_MODIFIED_AT)
|
||||
var modifiedAt: Long? = null
|
||||
)
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
package com.nextcloud.client.database.migrations
|
||||
|
||||
import androidx.room.DeleteColumn
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
|
@ -90,4 +91,12 @@ object DatabaseMigrationUtil {
|
|||
super.onPostMigrate(db)
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteColumn.Entries(
|
||||
DeleteColumn(
|
||||
tableName = "offline_operations",
|
||||
columnName = "offline_operations_parent_path"
|
||||
)
|
||||
)
|
||||
class DeleteColumnSpec : AutoMigrationSpec
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.database.typeAdapter
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonSerializationContext
|
||||
import com.google.gson.JsonSerializer
|
||||
import com.nextcloud.model.OfflineOperationRawType
|
||||
import com.nextcloud.model.OfflineOperationType
|
||||
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class OfflineOperationTypeAdapter : JsonSerializer<OfflineOperationType>, JsonDeserializer<OfflineOperationType> {
|
||||
|
||||
override fun serialize(
|
||||
src: OfflineOperationType?,
|
||||
typeOfSrc: Type?,
|
||||
context: JsonSerializationContext?
|
||||
): JsonElement {
|
||||
val jsonObject = JsonObject()
|
||||
jsonObject.addProperty("type", src?.javaClass?.simpleName)
|
||||
when (src) {
|
||||
is OfflineOperationType.CreateFolder -> {
|
||||
jsonObject.addProperty("type", src.type)
|
||||
jsonObject.addProperty("path", src.path)
|
||||
}
|
||||
|
||||
is OfflineOperationType.CreateFile -> {
|
||||
jsonObject.addProperty("type", src.type)
|
||||
jsonObject.addProperty("localPath", src.localPath)
|
||||
jsonObject.addProperty("remotePath", src.remotePath)
|
||||
jsonObject.addProperty("mimeType", src.mimeType)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
return jsonObject
|
||||
}
|
||||
|
||||
override fun deserialize(
|
||||
json: JsonElement?,
|
||||
typeOfT: Type?,
|
||||
context: JsonDeserializationContext?
|
||||
): OfflineOperationType? {
|
||||
val jsonObject = json?.asJsonObject ?: return null
|
||||
val type = jsonObject.get("type")?.asString
|
||||
return when (type) {
|
||||
OfflineOperationRawType.CreateFolder.name -> OfflineOperationType.CreateFolder(
|
||||
jsonObject.get("type").asString,
|
||||
jsonObject.get("path").asString
|
||||
)
|
||||
|
||||
OfflineOperationRawType.CreateFile.name -> OfflineOperationType.CreateFile(
|
||||
jsonObject.get("type").asString,
|
||||
jsonObject.get("localPath").asString,
|
||||
jsonObject.get("remotePath").asString,
|
||||
jsonObject.get("mimeType").asString
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.database.typeConverter
|
||||
|
||||
import androidx.room.ProvidedTypeConverter
|
||||
import androidx.room.TypeConverter
|
||||
import com.google.gson.Gson
|
||||
import com.nextcloud.model.OfflineOperationType
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.nextcloud.client.database.typeAdapter.OfflineOperationTypeAdapter
|
||||
|
||||
@ProvidedTypeConverter
|
||||
class OfflineOperationTypeConverter {
|
||||
|
||||
private val gson: Gson = GsonBuilder()
|
||||
.registerTypeAdapter(OfflineOperationType::class.java, OfflineOperationTypeAdapter())
|
||||
.create()
|
||||
|
||||
@TypeConverter
|
||||
fun fromOfflineOperationType(type: OfflineOperationType?): String? {
|
||||
return gson.toJson(type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toOfflineOperationType(type: String?): OfflineOperationType? {
|
||||
return gson.fromJson(type, OfflineOperationType::class.java)
|
||||
}
|
||||
}
|
|
@ -104,7 +104,13 @@ class BackgroundJobFactory @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createOfflineOperationsWorker(context: Context, params: WorkerParameters): ListenableWorker {
|
||||
return OfflineOperationsWorker(accountManager.user, context, connectivityService, viewThemeUtils.get(), params)
|
||||
return OfflineOperationsWorker(
|
||||
accountManager.user,
|
||||
context,
|
||||
connectivityService,
|
||||
viewThemeUtils.get(),
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFilesExportWork(context: Context, params: WorkerParameters): ListenableWorker {
|
||||
|
|
|
@ -23,11 +23,14 @@ import com.owncloud.android.lib.common.OwnCloudClient
|
|||
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation
|
||||
import com.owncloud.android.operations.CreateFolderOperation
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class OfflineOperationsWorker(
|
||||
private val user: User,
|
||||
|
@ -48,7 +51,7 @@ class OfflineOperationsWorker(
|
|||
private var repository = OfflineOperationsRepository(fileDataStorageManager)
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override suspend fun doWork(): Result = coroutineScope {
|
||||
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
||||
val jobName = inputData.getString(JOB_NAME)
|
||||
Log_OC.d(
|
||||
TAG,
|
||||
|
@ -57,9 +60,9 @@ class OfflineOperationsWorker(
|
|||
"\n-----------------------------------"
|
||||
)
|
||||
|
||||
if (!connectivityService.isNetworkAndServerAvailable()) {
|
||||
if (!isNetworkAndServerAvailable()) {
|
||||
Log_OC.d(TAG, "OfflineOperationsWorker cancelled, no internet connection")
|
||||
return@coroutineScope Result.retry()
|
||||
return@withContext Result.retry()
|
||||
}
|
||||
|
||||
val client = clientFactory.create(user)
|
||||
|
@ -69,7 +72,7 @@ class OfflineOperationsWorker(
|
|||
val totalOperations = operations.size
|
||||
var currentSuccessfulOperationIndex = 0
|
||||
|
||||
return@coroutineScope try {
|
||||
return@withContext try {
|
||||
while (operations.isNotEmpty()) {
|
||||
val operation = operations.first()
|
||||
val result = executeOperation(operation, client)
|
||||
|
@ -99,27 +102,48 @@ class OfflineOperationsWorker(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("Deprecation")
|
||||
private suspend fun isNetworkAndServerAvailable(): Boolean = suspendCoroutine { continuation ->
|
||||
connectivityService.isNetworkAndServerAvailable { result ->
|
||||
continuation.resume(result)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("Deprecation", "MagicNumber")
|
||||
private suspend fun executeOperation(
|
||||
operation: OfflineOperationEntity,
|
||||
client: OwnCloudClient
|
||||
): Pair<RemoteOperationResult<*>?, RemoteOperation<*>?>? {
|
||||
return when (operation.type) {
|
||||
OfflineOperationType.CreateFolder -> {
|
||||
if (operation.parentPath != null) {
|
||||
val createFolderOperation = withContext(Dispatchers.IO) {
|
||||
CreateFolderOperation(
|
||||
operation.path,
|
||||
user,
|
||||
context,
|
||||
fileDataStorageManager
|
||||
)
|
||||
}
|
||||
createFolderOperation.execute(client) to createFolderOperation
|
||||
} else {
|
||||
Log_OC.d(TAG, "CreateFolder operation incomplete, missing parentPath")
|
||||
null
|
||||
): Pair<RemoteOperationResult<*>?, RemoteOperation<*>?>? = withContext(Dispatchers.IO) {
|
||||
return@withContext when (operation.type) {
|
||||
is OfflineOperationType.CreateFolder -> {
|
||||
val createFolderOperation = withContext(NonCancellable) {
|
||||
val operationType = (operation.type as OfflineOperationType.CreateFolder)
|
||||
CreateFolderOperation(
|
||||
operationType.path,
|
||||
user,
|
||||
context,
|
||||
fileDataStorageManager
|
||||
)
|
||||
}
|
||||
createFolderOperation.execute(client) to createFolderOperation
|
||||
}
|
||||
|
||||
is OfflineOperationType.CreateFile -> {
|
||||
val createFileOperation = withContext(NonCancellable) {
|
||||
val operationType = (operation.type as OfflineOperationType.CreateFile)
|
||||
val lastModificationDate = System.currentTimeMillis() / 1000
|
||||
|
||||
UploadFileRemoteOperation(
|
||||
operationType.localPath,
|
||||
operationType.remotePath,
|
||||
operationType.mimeType,
|
||||
"",
|
||||
operation.modifiedAt ?: lastModificationDate,
|
||||
operation.createdAt ?: System.currentTimeMillis(),
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
createFileOperation.execute(client) to createFileOperation
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
@ -142,7 +166,7 @@ class OfflineOperationsWorker(
|
|||
}
|
||||
|
||||
val logMessage = if (result.isSuccess) "Operation completed" else "Operation failed"
|
||||
Log_OC.d(TAG, "$logMessage path: ${operation.path}, type: ${operation.type}")
|
||||
Log_OC.d(TAG, "$logMessage filename: ${operation.filename}, type: ${operation.type}")
|
||||
|
||||
if (result.isSuccess) {
|
||||
repository.updateNextOperations(operation)
|
||||
|
|
|
@ -8,8 +8,11 @@
|
|||
package com.nextcloud.client.jobs.offlineOperations.repository
|
||||
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
import com.nextcloud.model.OfflineOperationType
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.utils.MimeType
|
||||
import com.owncloud.android.utils.MimeTypeUtil
|
||||
|
||||
class OfflineOperationsRepository(
|
||||
private val fileDataStorageManager: FileDataStorageManager
|
||||
|
@ -19,7 +22,7 @@ class OfflineOperationsRepository(
|
|||
private val pathSeparator = '/'
|
||||
|
||||
@Suppress("NestedBlockDepth")
|
||||
override fun getAllSubdirectories(fileId: Long): List<OfflineOperationEntity> {
|
||||
override fun getAllSubEntities(fileId: Long): List<OfflineOperationEntity> {
|
||||
val result = mutableListOf<OfflineOperationEntity>()
|
||||
val queue = ArrayDeque<Long>()
|
||||
queue.add(fileId)
|
||||
|
@ -31,7 +34,7 @@ class OfflineOperationsRepository(
|
|||
|
||||
processedIds.add(currentFileId)
|
||||
|
||||
val subDirectories = dao.getSubDirectoriesByParentOCFileId(currentFileId)
|
||||
val subDirectories = dao.getSubEntitiesByParentOCFileId(currentFileId)
|
||||
result.addAll(subDirectories)
|
||||
|
||||
subDirectories.forEach {
|
||||
|
@ -48,15 +51,14 @@ class OfflineOperationsRepository(
|
|||
}
|
||||
|
||||
override fun deleteOperation(file: OCFile) {
|
||||
getAllSubdirectories(file.fileId).forEach {
|
||||
dao.delete(it)
|
||||
if (file.isFolder) {
|
||||
getAllSubEntities(file.fileId).forEach {
|
||||
dao.delete(it)
|
||||
}
|
||||
}
|
||||
|
||||
file.decryptedRemotePath?.let {
|
||||
val entity = dao.getByPath(it)
|
||||
entity?.let {
|
||||
dao.delete(entity)
|
||||
}
|
||||
dao.deleteByPath(it)
|
||||
}
|
||||
|
||||
fileDataStorageManager.removeFile(file, true, true)
|
||||
|
@ -66,17 +68,28 @@ class OfflineOperationsRepository(
|
|||
val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(operation.path)
|
||||
val fileId = ocFile?.fileId ?: return
|
||||
|
||||
getAllSubdirectories(fileId)
|
||||
getAllSubEntities(fileId)
|
||||
.mapNotNull { nextOperation ->
|
||||
nextOperation.parentOCFileId?.let { parentId ->
|
||||
fileDataStorageManager.getFileById(parentId)?.let { ocFile ->
|
||||
ocFile.decryptedRemotePath?.let { updatedPath ->
|
||||
val newParentPath = ocFile.parentRemotePath
|
||||
val newPath = updatedPath + nextOperation.filename + pathSeparator
|
||||
|
||||
if (newParentPath != nextOperation.parentPath || newPath != nextOperation.path) {
|
||||
if (newPath != nextOperation.path) {
|
||||
nextOperation.apply {
|
||||
parentPath = newParentPath
|
||||
type = when (type) {
|
||||
is OfflineOperationType.CreateFile ->
|
||||
(type as OfflineOperationType.CreateFile).copy(
|
||||
remotePath = newPath
|
||||
)
|
||||
|
||||
is OfflineOperationType.CreateFolder ->
|
||||
(type as OfflineOperationType.CreateFolder).copy(
|
||||
path = newPath
|
||||
)
|
||||
|
||||
else -> type
|
||||
}
|
||||
path = newPath
|
||||
}
|
||||
} else {
|
||||
|
@ -88,4 +101,15 @@ class OfflineOperationsRepository(
|
|||
}
|
||||
.forEach { dao.update(it) }
|
||||
}
|
||||
|
||||
override fun convertToOCFiles(fileId: Long): List<OCFile> =
|
||||
dao.getSubEntitiesByParentOCFileId(fileId).map { entity ->
|
||||
OCFile(entity.path).apply {
|
||||
mimeType = if (entity.type is OfflineOperationType.CreateFolder) {
|
||||
MimeType.DIRECTORY
|
||||
} else {
|
||||
MimeTypeUtil.getMimeTypeFromPath(entity.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@ import com.nextcloud.client.database.entity.OfflineOperationEntity
|
|||
import com.owncloud.android.datamodel.OCFile
|
||||
|
||||
interface OfflineOperationsRepositoryType {
|
||||
fun getAllSubdirectories(fileId: Long): List<OfflineOperationEntity>
|
||||
fun getAllSubEntities(fileId: Long): List<OfflineOperationEntity>
|
||||
fun deleteOperation(file: OCFile)
|
||||
fun updateNextOperations(operation: OfflineOperationEntity)
|
||||
fun convertToOCFiles(fileId: Long): List<OCFile>
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
*/
|
||||
package com.nextcloud.client.network;
|
||||
|
||||
import android.os.NetworkOnMainThreadException;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* This service provides information about current network connectivity
|
||||
|
@ -17,16 +18,12 @@ public interface ConnectivityService {
|
|||
* Checks the availability of the server and the device's internet connection.
|
||||
* <p>
|
||||
* This method performs a network request to verify if the server is accessible and
|
||||
* checks if the device has an active internet connection. Due to the network operations involved,
|
||||
* this method should be executed on a background thread to avoid blocking the main thread.
|
||||
* checks if the device has an active internet connection.
|
||||
* </p>
|
||||
*
|
||||
* @return {@code true} if the server is accessible and the device has an internet connection;
|
||||
* {@code false} otherwise.
|
||||
*
|
||||
* @throws NetworkOnMainThreadException if this function runs on main thread.
|
||||
* @param callback A callback to handle the result of the network and server availability check.
|
||||
*/
|
||||
boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException;
|
||||
void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback);
|
||||
|
||||
boolean isConnected();
|
||||
|
||||
|
@ -45,4 +42,13 @@ public interface ConnectivityService {
|
|||
* @return Network connectivity status in platform-agnostic format
|
||||
*/
|
||||
Connectivity getConnectivity();
|
||||
|
||||
/**
|
||||
* Callback interface for asynchronous results.
|
||||
*
|
||||
* @param <T> The type of result returned by the callback.
|
||||
*/
|
||||
interface GenericCallback<T> {
|
||||
void onComplete(T result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,8 @@ import android.net.ConnectivityManager;
|
|||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.NetworkOnMainThreadException;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import com.nextcloud.client.account.Server;
|
||||
import com.nextcloud.client.account.UserAccountManager;
|
||||
|
@ -23,6 +24,7 @@ import com.owncloud.android.lib.common.utils.Log_OC;
|
|||
|
||||
import org.apache.commons.httpclient.HttpStatus;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.net.ConnectivityManagerCompat;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
|
||||
|
@ -36,6 +38,7 @@ class ConnectivityServiceImpl implements ConnectivityService {
|
|||
private final ClientFactory clientFactory;
|
||||
private final GetRequestBuilder requestBuilder;
|
||||
private final WalledCheckCache walledCheckCache;
|
||||
private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
static class GetRequestBuilder implements Function1<String, GetMethod> {
|
||||
@Override
|
||||
|
@ -57,16 +60,21 @@ class ConnectivityServiceImpl implements ConnectivityService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
|
||||
Network activeNetwork = platformConnectivityManager.getActiveNetwork();
|
||||
NetworkCapabilities networkCapabilities = platformConnectivityManager.getNetworkCapabilities(activeNetwork);
|
||||
boolean hasInternet = networkCapabilities != null && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
|
||||
public void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback) {
|
||||
new Thread(() -> {
|
||||
Network activeNetwork = platformConnectivityManager.getActiveNetwork();
|
||||
NetworkCapabilities networkCapabilities = platformConnectivityManager.getNetworkCapabilities(activeNetwork);
|
||||
boolean hasInternet = networkCapabilities != null && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
|
||||
|
||||
if (!hasInternet) {
|
||||
return false;
|
||||
}
|
||||
boolean result;
|
||||
if (hasInternet) {
|
||||
result = !isInternetWalled();
|
||||
} else {
|
||||
result = false;
|
||||
}
|
||||
|
||||
return !isInternetWalled();
|
||||
mainThreadHandler.post(() -> callback.onComplete(result));
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -7,6 +7,19 @@
|
|||
|
||||
package com.nextcloud.model
|
||||
|
||||
enum class OfflineOperationType {
|
||||
CreateFolder
|
||||
sealed class OfflineOperationType {
|
||||
abstract val type: String
|
||||
|
||||
data class CreateFolder(override val type: String, var path: String) : OfflineOperationType()
|
||||
data class CreateFile(
|
||||
override val type: String,
|
||||
val localPath: String,
|
||||
var remotePath: String,
|
||||
val mimeType: String
|
||||
) : OfflineOperationType()
|
||||
}
|
||||
|
||||
enum class OfflineOperationRawType {
|
||||
CreateFolder,
|
||||
CreateFile
|
||||
}
|
||||
|
|
|
@ -11,9 +11,6 @@ import android.content.BroadcastReceiver
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.nextcloud.client.network.ConnectivityService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
interface NetworkChangeListener {
|
||||
fun networkAndServerConnectionListener(isNetworkAndServerAvailable: Boolean)
|
||||
|
@ -24,15 +21,9 @@ class NetworkChangeReceiver(
|
|||
private val connectivityService: ConnectivityService
|
||||
) : BroadcastReceiver() {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
scope.launch {
|
||||
val isNetworkAndServerAvailable = connectivityService.isNetworkAndServerAvailable()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
listener.networkAndServerConnectionListener(isNetworkAndServerAvailable)
|
||||
}
|
||||
connectivityService.isNetworkAndServerAvailable {
|
||||
listener.networkAndServerConnectionListener(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,5 @@ class OfflineOperationActionReceiver : BroadcastReceiver() {
|
|||
val user = intent.getParcelableArgument(USER, User::class.java) ?: return
|
||||
val fileDataStorageManager = FileDataStorageManager(user, context?.contentResolver)
|
||||
fileDataStorageManager.offlineOperationDao.deleteByPath(path)
|
||||
// TODO Update notification
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,3 +16,14 @@ inline fun <reified T : Any> Fragment.typedActivity(): T? {
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension for Java Classes
|
||||
*/
|
||||
fun <T : Any> Fragment.getTypedActivity(type: Class<T>): T? {
|
||||
return if (isAdded && activity != null && type.isInstance(activity)) {
|
||||
type.cast(activity)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ object FileNameValidator {
|
|||
* @param existedFileNames Set of existing file names to avoid duplicates.
|
||||
* @return An error message if the filename is invalid, null otherwise.
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
@Suppress("ReturnCount", "NestedBlockDepth")
|
||||
fun checkFileName(
|
||||
filename: String,
|
||||
capability: OCCapability,
|
||||
|
|
|
@ -41,6 +41,7 @@ import com.nextcloud.client.database.entity.OfflineOperationEntity;
|
|||
import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepository;
|
||||
import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepositoryType;
|
||||
import com.nextcloud.model.OCFileFilterType;
|
||||
import com.nextcloud.model.OfflineOperationRawType;
|
||||
import com.nextcloud.model.OfflineOperationType;
|
||||
import com.nextcloud.utils.date.DateFormatPattern;
|
||||
import com.nextcloud.utils.extensions.DateExtensionsKt;
|
||||
|
@ -107,7 +108,7 @@ public class FileDataStorageManager {
|
|||
public final OfflineOperationDao offlineOperationDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).offlineOperationDao();
|
||||
private final FileDao fileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).fileDao();
|
||||
private final Gson gson = new Gson();
|
||||
private final OfflineOperationsRepositoryType offlineOperationsRepository;
|
||||
public final OfflineOperationsRepositoryType offlineOperationsRepository;
|
||||
|
||||
public FileDataStorageManager(User user, ContentResolver contentResolver) {
|
||||
this.contentProviderClient = null;
|
||||
|
@ -140,33 +141,83 @@ public class FileDataStorageManager {
|
|||
return getFileByPath(ProviderTableMeta.FILE_PATH_DECRYPTED, path);
|
||||
}
|
||||
|
||||
public OfflineOperationEntity addCreateFolderOfflineOperation(String path, String filename, String parentPath, Long parentOCFileId) {
|
||||
public void addCreateFileOfflineOperation(String[] localPaths, String[] remotePaths) {
|
||||
if (localPaths.length != remotePaths.length) {
|
||||
Log_OC.d(TAG, "Local path and remote path size do not match");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < localPaths.length; i++) {
|
||||
String localPath = localPaths[i];
|
||||
String remotePath = remotePaths[i];
|
||||
String mimeType = MimeTypeUtil.getMimeTypeFromPath(remotePath);
|
||||
|
||||
OfflineOperationEntity entity = new OfflineOperationEntity();
|
||||
entity.setPath(remotePath);
|
||||
entity.setType(new OfflineOperationType.CreateFile(OfflineOperationRawType.CreateFile.name(), localPath, remotePath, mimeType));
|
||||
|
||||
long createdAt = System.currentTimeMillis();
|
||||
long modificationTimestamp = System.currentTimeMillis();
|
||||
|
||||
entity.setCreatedAt(createdAt);
|
||||
entity.setModifiedAt(modificationTimestamp / 1000);
|
||||
entity.setFilename(new File(remotePath).getName());
|
||||
|
||||
String parentPath = new File(remotePath).getParent() + OCFile.PATH_SEPARATOR;
|
||||
OCFile parentFile = getFileByDecryptedRemotePath(parentPath);
|
||||
|
||||
if (parentFile != null) {
|
||||
entity.setParentOCFileId(parentFile.getFileId());
|
||||
}
|
||||
|
||||
offlineOperationDao.insert(entity);
|
||||
createPendingFile(remotePath, mimeType, createdAt, modificationTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
public OfflineOperationEntity addCreateFolderOfflineOperation(String path, String filename, Long parentOCFileId) {
|
||||
OfflineOperationEntity entity = new OfflineOperationEntity();
|
||||
|
||||
entity.setFilename(filename);
|
||||
entity.setParentOCFileId(parentOCFileId);
|
||||
|
||||
OfflineOperationType.CreateFolder operationType = new OfflineOperationType.CreateFolder(OfflineOperationRawType.CreateFolder.name(), path);
|
||||
entity.setType(operationType);
|
||||
entity.setPath(path);
|
||||
entity.setParentPath(parentPath);
|
||||
entity.setCreatedAt(System.currentTimeMillis() / 1000L);
|
||||
entity.setType(OfflineOperationType.CreateFolder);
|
||||
|
||||
long createdAt = System.currentTimeMillis();
|
||||
long modificationTimestamp = System.currentTimeMillis();
|
||||
|
||||
entity.setCreatedAt(createdAt);
|
||||
entity.setModifiedAt(modificationTimestamp / 1000);
|
||||
|
||||
offlineOperationDao.insert(entity);
|
||||
createPendingDirectory(path);
|
||||
createPendingDirectory(path, createdAt, modificationTimestamp);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
public void createPendingDirectory(String path) {
|
||||
public void createPendingFile(String path, String mimeType, long createdAt, long modificationTimestamp) {
|
||||
OCFile file = new OCFile(path);
|
||||
file.setMimeType(MimeType.DIRECTORY);
|
||||
file.setMimeType(mimeType);
|
||||
file.setCreationTimestamp(createdAt);
|
||||
file.setModificationTimestamp(modificationTimestamp);
|
||||
saveFileWithParent(file, MainApp.getAppContext());
|
||||
}
|
||||
|
||||
public void createPendingDirectory(String path, long createdAt, long modificationTimestamp) {
|
||||
OCFile directory = new OCFile(path);
|
||||
directory.setMimeType(MimeType.DIRECTORY);
|
||||
directory.setCreationTimestamp(createdAt);
|
||||
directory.setModificationTimestamp(modificationTimestamp);
|
||||
saveFileWithParent(directory, MainApp.getAppContext());
|
||||
}
|
||||
|
||||
public void deleteOfflineOperation(OCFile file) {
|
||||
offlineOperationsRepository.deleteOperation(file);
|
||||
}
|
||||
|
||||
public void renameCreateFolderOfflineOperation(OCFile file, String newFolderName) {
|
||||
public void renameOfflineOperation(OCFile file, String newFolderName) {
|
||||
var entity = offlineOperationDao.getByPath(file.getDecryptedRemotePath());
|
||||
if (entity == null) {
|
||||
return;
|
||||
|
@ -178,6 +229,14 @@ public class FileDataStorageManager {
|
|||
}
|
||||
|
||||
String newPath = parentFolder.getDecryptedRemotePath() + newFolderName + OCFile.PATH_SEPARATOR;
|
||||
|
||||
if (entity.getType() instanceof OfflineOperationType.CreateFolder createFolderType) {
|
||||
createFolderType.setPath(newPath);
|
||||
} else if (entity.getType() instanceof OfflineOperationType.CreateFile createFileType) {
|
||||
createFileType.setRemotePath(newPath);
|
||||
}
|
||||
entity.setType(entity.getType());
|
||||
|
||||
entity.setPath(newPath);
|
||||
entity.setFilename(newFolderName);
|
||||
offlineOperationDao.update(entity);
|
||||
|
|
|
@ -788,18 +788,6 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
|
|||
return getRemoteId() == null;
|
||||
}
|
||||
|
||||
public String getOfflineOperationParentPath() {
|
||||
if (isOfflineOperation()) {
|
||||
if (Objects.equals(remotePath, OCFile.PATH_SEPARATOR)) {
|
||||
return OCFile.PATH_SEPARATOR;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return getDecryptedRemotePath();
|
||||
}
|
||||
}
|
||||
|
||||
public String getEtagInConflict() {
|
||||
return this.etagInConflict;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import java.util.List;
|
|||
*/
|
||||
public class ProviderMeta {
|
||||
public static final String DB_NAME = "filelist";
|
||||
public static final int DB_VERSION = 84;
|
||||
public static final int DB_VERSION = 85;
|
||||
|
||||
private ProviderMeta() {
|
||||
// No instance
|
||||
|
@ -289,9 +289,9 @@ public class ProviderMeta {
|
|||
|
||||
// Columns of offline operation table
|
||||
public static final String OFFLINE_OPERATION_PARENT_OC_FILE_ID = "offline_operations_parent_oc_file_id";
|
||||
public static final String OFFLINE_OPERATION_PARENT_PATH = "offline_operations_parent_path";
|
||||
public static final String OFFLINE_OPERATION_TYPE = "offline_operations_type";
|
||||
public static final String OFFLINE_OPERATION_PATH = "offline_operations_path";
|
||||
public static final String OFFLINE_OPERATION_MODIFIED_AT = "offline_operations_modified_at";
|
||||
public static final String OFFLINE_OPERATION_CREATED_AT = "offline_operations_created_at";
|
||||
public static final String OFFLINE_OPERATION_FILE_NAME = "offline_operations_file_name";
|
||||
|
||||
|
|
|
@ -165,8 +165,7 @@ public abstract class FileActivity extends DrawerActivity
|
|||
@Inject
|
||||
UserAccountManager accountManager;
|
||||
|
||||
@Inject
|
||||
ConnectivityService connectivityService;
|
||||
@Inject public ConnectivityService connectivityService;
|
||||
|
||||
@Inject
|
||||
BackgroundJobManager backgroundJobManager;
|
||||
|
@ -246,6 +245,7 @@ public abstract class FileActivity extends DrawerActivity
|
|||
public void networkAndServerConnectionListener(boolean isNetworkAndServerAvailable) {
|
||||
if (isNetworkAndServerAvailable) {
|
||||
hideInfoBox();
|
||||
refreshList();
|
||||
} else {
|
||||
showInfoBox(R.string.offline_mode);
|
||||
}
|
||||
|
|
|
@ -236,8 +236,6 @@ public class FileDisplayActivity extends FileActivity
|
|||
|
||||
@Inject AppInfo appInfo;
|
||||
|
||||
@Inject ConnectivityService connectivityService;
|
||||
|
||||
@Inject InAppReviewHelper inAppReviewHelper;
|
||||
|
||||
@Inject FastScrollUtils fastScrollUtils;
|
||||
|
@ -952,16 +950,21 @@ public class FileDisplayActivity extends FileActivity
|
|||
default -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET;
|
||||
};
|
||||
|
||||
FileUploadHelper.Companion.instance().uploadNewFiles(getUser().orElseThrow(RuntimeException::new),
|
||||
filePaths,
|
||||
decryptedRemotePaths,
|
||||
behaviour,
|
||||
true,
|
||||
UploadFileOperation.CREATED_BY_USER,
|
||||
false,
|
||||
false,
|
||||
NameCollisionPolicy.ASK_USER);
|
||||
|
||||
connectivityService.isNetworkAndServerAvailable(result -> {
|
||||
if (result) {
|
||||
FileUploadHelper.Companion.instance().uploadNewFiles(getUser().orElseThrow(RuntimeException::new),
|
||||
filePaths,
|
||||
decryptedRemotePaths,
|
||||
behaviour,
|
||||
true,
|
||||
UploadFileOperation.CREATED_BY_USER,
|
||||
false,
|
||||
false,
|
||||
NameCollisionPolicy.ASK_USER);
|
||||
} else {
|
||||
fileDataStorageManager.addCreateFileOfflineOperation(filePaths, decryptedRemotePaths);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Log_OC.d(TAG, "User clicked on 'Update' with no selection");
|
||||
DisplayUtils.showSnackMessage(this, R.string.filedisplay_no_file_selected);
|
||||
|
@ -1379,7 +1382,13 @@ public class FileDisplayActivity extends FileActivity
|
|||
if (MainApp.isOnlyOnDevice()) {
|
||||
ocFileListFragment.setMessageForEmptyList(R.string.file_list_empty_headline, R.string.file_list_empty_on_device, R.drawable.ic_list_empty_folder, true);
|
||||
} else {
|
||||
ocFileListFragment.setEmptyListMessage(SearchType.NO_SEARCH);
|
||||
connectivityService.isNetworkAndServerAvailable(result -> {
|
||||
if (result) {
|
||||
ocFileListFragment.setEmptyListMessage(SearchType.NO_SEARCH);
|
||||
} else {
|
||||
ocFileListFragment.setEmptyListMessage(SearchType.OFFLINE_MODE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -27,7 +27,6 @@ import com.nextcloud.client.device.PowerManagementService;
|
|||
import com.nextcloud.client.jobs.BackgroundJobManager;
|
||||
import com.nextcloud.client.jobs.upload.FileUploadHelper;
|
||||
import com.nextcloud.client.jobs.upload.FileUploadWorker;
|
||||
import com.nextcloud.client.network.ConnectivityService;
|
||||
import com.nextcloud.client.utils.Throttler;
|
||||
import com.nextcloud.model.WorkerState;
|
||||
import com.nextcloud.model.WorkerStateLiveData;
|
||||
|
@ -44,7 +43,6 @@ import com.owncloud.android.ui.adapter.UploadListAdapter;
|
|||
import com.owncloud.android.ui.decoration.MediaGridItemDecoration;
|
||||
import com.owncloud.android.utils.DisplayUtils;
|
||||
import com.owncloud.android.utils.FilesSyncHelper;
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
|
@ -73,9 +71,6 @@ public class UploadListActivity extends FileActivity {
|
|||
@Inject
|
||||
UploadsStorageManager uploadsStorageManager;
|
||||
|
||||
@Inject
|
||||
ConnectivityService connectivityService;
|
||||
|
||||
@Inject
|
||||
PowerManagementService powerManagementService;
|
||||
|
||||
|
|
|
@ -16,9 +16,11 @@ import android.annotation.SuppressLint;
|
|||
import android.app.Activity;
|
||||
import android.content.ContentValues;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
|
@ -31,8 +33,10 @@ import android.widget.LinearLayout;
|
|||
import com.elyeproj.loaderviewlibrary.LoaderImageView;
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole;
|
||||
import com.nextcloud.client.account.User;
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity;
|
||||
import com.nextcloud.client.jobs.upload.FileUploadHelper;
|
||||
import com.nextcloud.client.preferences.AppPreferences;
|
||||
import com.nextcloud.model.OfflineOperationType;
|
||||
import com.nextcloud.model.OCFileFilterType;
|
||||
import com.nextcloud.utils.extensions.ViewExtensionsKt;
|
||||
import com.owncloud.android.MainApp;
|
||||
|
@ -66,6 +70,7 @@ import com.owncloud.android.ui.activity.FileDisplayActivity;
|
|||
import com.owncloud.android.ui.fragment.SearchType;
|
||||
import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface;
|
||||
import com.owncloud.android.ui.preview.PreviewTextFragment;
|
||||
import com.owncloud.android.utils.BitmapUtils;
|
||||
import com.owncloud.android.utils.DisplayUtils;
|
||||
import com.owncloud.android.utils.FileSortOrder;
|
||||
import com.owncloud.android.utils.FileStorageUtils;
|
||||
|
@ -81,8 +86,12 @@ import java.util.Collections;
|
|||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -161,7 +170,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
userId = AccountManager
|
||||
.get(activity)
|
||||
.getUserData(this.user.toPlatformAccount(),
|
||||
com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
|
||||
AccountUtils.Constants.KEY_USER_ID);
|
||||
|
||||
this.viewThemeUtils = viewThemeUtils;
|
||||
|
||||
|
@ -523,7 +532,11 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
}
|
||||
|
||||
ViewExtensionsKt.setVisibleIf(holder.getShared(), !file.isOfflineOperation());
|
||||
setColorFilterForOfflineOperations(holder, file);
|
||||
if (file.isFolder()) {
|
||||
setColorFilterForOfflineCreateFolderOperations(holder, file);
|
||||
} else {
|
||||
setColorFilterForOfflineCreateFileOperations(holder, file);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) {
|
||||
|
@ -596,13 +609,14 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
|
||||
holder.getFileSize().setVisibility(View.VISIBLE);
|
||||
|
||||
|
||||
if (file.isOfflineOperation()) {
|
||||
holder.getFileSize().setText(MainApp.string(R.string.oc_file_list_adapter_offline_operation_description_text));
|
||||
holder.getFileSizeSeparator().setVisibility(View.GONE);
|
||||
} else {
|
||||
holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(localSize));
|
||||
holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
final long fileLength = file.getFileLength();
|
||||
if (fileLength >= 0) {
|
||||
|
@ -610,11 +624,11 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
|
||||
if (file.isOfflineOperation()) {
|
||||
holder.getFileSize().setText(MainApp.string(R.string.oc_file_list_adapter_offline_operation_description_text));
|
||||
holder.getFileSizeSeparator().setVisibility(View.GONE);
|
||||
} else {
|
||||
holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(fileLength));
|
||||
holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.getFileSize().setVisibility(View.GONE);
|
||||
holder.getFileSizeSeparator().setVisibility(View.GONE);
|
||||
|
@ -654,14 +668,40 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
|
||||
private void applyVisualsForOfflineOperations(ListItemViewHolder holder, OCFile file) {
|
||||
ViewExtensionsKt.setVisibleIf(holder.getShared(), !file.isOfflineOperation());
|
||||
setColorFilterForOfflineOperations(holder, file);
|
||||
|
||||
if (file.isFolder()) {
|
||||
setColorFilterForOfflineCreateFolderOperations(holder, file);
|
||||
} else {
|
||||
setColorFilterForOfflineCreateFileOperations(holder, file);
|
||||
}
|
||||
}
|
||||
|
||||
private void setColorFilterForOfflineOperations(ListViewHolder holder, OCFile file) {
|
||||
if (!file.isFolder()) {
|
||||
private final ExecutorService executorService = Executors.newCachedThreadPool();
|
||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
private void setColorFilterForOfflineCreateFileOperations(ListViewHolder holder, OCFile file) {
|
||||
if (!file.isOfflineOperation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
executorService.execute(() -> {
|
||||
OfflineOperationEntity entity = mStorageManager.offlineOperationDao.getByPath(file.getDecryptedRemotePath());
|
||||
|
||||
if (entity != null && entity.getType() != null && entity.getType() instanceof OfflineOperationType.CreateFile createFileOperation) {
|
||||
Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(createFileOperation.getLocalPath(), holder.getThumbnail().getWidth(), holder.getThumbnail().getHeight());
|
||||
if (bitmap == null) return;
|
||||
|
||||
Bitmap thumbnail = BitmapUtils.addColorFilter(bitmap, Color.GRAY,100);
|
||||
mainHandler.post(() -> holder.getThumbnail().setImageBitmap(thumbnail));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void onDestroy() {
|
||||
executorService.shutdown();
|
||||
}
|
||||
|
||||
private void setColorFilterForOfflineCreateFolderOperations(ListViewHolder holder, OCFile file) {
|
||||
if (file.isOfflineOperation()) {
|
||||
holder.getThumbnail().setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_IN);
|
||||
} else {
|
||||
|
@ -782,6 +822,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
prepareListOfHiddenFiles();
|
||||
mergeOCFilesForLivePhoto();
|
||||
mFilesAll.clear();
|
||||
addOfflineOperations(directory.getFileId());
|
||||
mFilesAll.addAll(mFiles);
|
||||
currentDirectory = directory;
|
||||
} else {
|
||||
|
@ -790,10 +831,39 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
}
|
||||
|
||||
searchType = null;
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Offline Operations to OCFiles and adds them to the adapter for visual feedback.
|
||||
* This function creates pending OCFiles, but they may not consistently appear in the UI.
|
||||
* The issue arises when {@link RefreshFolderOperation} deletes pending Offline Operations, while some may still exist in the table.
|
||||
* If only this function is used, it cause crash in {@link FileDisplayActivity mSyncBroadcastReceiver.onReceive}.
|
||||
* <p>
|
||||
* These function also need to be used: {@link FileDataStorageManager#createPendingDirectory(String, long, long)}, {@link FileDataStorageManager#createPendingFile(String, String, long, long)}.
|
||||
*/
|
||||
private void addOfflineOperations(long fileId) {
|
||||
List<OCFile> offlineOperations = mStorageManager.offlineOperationsRepository.convertToOCFiles(fileId);
|
||||
if (offlineOperations.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<OCFile> newFiles;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
newFiles = offlineOperations.stream()
|
||||
.filter(offlineFile -> mFilesAll.stream()
|
||||
.noneMatch(file -> Objects.equals(file.getDecryptedRemotePath(), offlineFile.getDecryptedRemotePath())))
|
||||
.toList();
|
||||
} else {
|
||||
newFiles = offlineOperations.stream()
|
||||
.filter(offlineFile -> mFilesAll.stream()
|
||||
.noneMatch(file -> Objects.equals(file.getDecryptedRemotePath(), offlineFile.getDecryptedRemotePath())))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
mFilesAll.addAll(newFiles);
|
||||
}
|
||||
|
||||
public void setData(List<Object> objects,
|
||||
SearchType searchType,
|
||||
FileDataStorageManager storageManager,
|
||||
|
|
|
@ -16,11 +16,15 @@ object OCShareToOCFileConverter {
|
|||
private const val MILLIS_PER_SECOND = 1000
|
||||
|
||||
/**
|
||||
* Generates a list of incomplete [OCFile] from a list of [OCShare]
|
||||
* Generates a list of incomplete [OCFile] from a list of [OCShare]. Retrieving OCFile directly by path may fail
|
||||
* in cases like
|
||||
* when a shared file is located at a/b/c/d/a.txt. To display a.txt in the shared tab, the device needs the OCFile.
|
||||
* On first launch, the app may not be aware of the file until the exact path is accessed.
|
||||
*
|
||||
* This is actually pretty complex as we get one [OCShare] item for each shared instance for the same folder
|
||||
* Server implementation needed to get file size, thumbnails e.g. :
|
||||
* <a href="https://github.com/nextcloud/server/issues/4456g</a>.
|
||||
*
|
||||
* **THIS ONLY WORKS WITH FILES SHARED *BY* THE USER, NOT FOR SHARES *WITH* THE USER**
|
||||
* Note: This works only for files shared *by* the user, not files shared *with* the user.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun buildOCFilesFromShares(shares: List<OCShare>): List<OCFile> {
|
||||
|
|
|
@ -56,7 +56,6 @@ import com.owncloud.android.utils.theme.ViewThemeUtils;
|
|||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -119,14 +118,20 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
|
|||
|
||||
headerViewHolder.binding.uploadListAction.setOnClickListener(v -> {
|
||||
switch (group.type) {
|
||||
case CURRENT -> {
|
||||
new Thread(() -> {
|
||||
uploadHelper.cancelFileUploads(
|
||||
Arrays.asList(group.items),
|
||||
group.getItem(0).getAccountName());
|
||||
parentActivity.runOnUiThread(this::loadUploadItemsFromDb);
|
||||
}).start();
|
||||
}
|
||||
case CURRENT -> new Thread(() -> {
|
||||
OCUpload ocUpload = group.getItem(0);
|
||||
if (ocUpload == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String accountName = ocUpload.getAccountName();
|
||||
if (accountName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
uploadHelper.cancelFileUploads(Arrays.asList(group.items), accountName);
|
||||
parentActivity.runOnUiThread(this::loadUploadItemsFromDb);
|
||||
}).start();
|
||||
case FINISHED -> {
|
||||
uploadsStorageManager.clearSuccessfulUploads();
|
||||
loadUploadItemsFromDb();
|
||||
|
@ -287,16 +292,27 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
|
|||
|
||||
@Override
|
||||
public void onBindViewHolder(SectionedViewHolder holder, int section, int relativePosition, int absolutePosition) {
|
||||
if (uploadGroups.length == 0 || section < 0 || section >= uploadGroups.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
UploadGroup uploadGroup = uploadGroups[section];
|
||||
if (uploadGroup == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
OCUpload item = uploadGroup.getItem(relativePosition);
|
||||
if (item == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ItemViewHolder itemViewHolder = (ItemViewHolder) holder;
|
||||
|
||||
OCUpload item = uploadGroups[section].getItem(relativePosition);
|
||||
|
||||
itemViewHolder.binding.uploadName.setText(item.getLocalPath());
|
||||
|
||||
// local file name
|
||||
File remoteFile = new File(item.getRemotePath());
|
||||
String fileName = remoteFile.getName();
|
||||
if (fileName.length() == 0) {
|
||||
if (fileName.isEmpty()) {
|
||||
fileName = File.separator;
|
||||
}
|
||||
itemViewHolder.binding.uploadName.setText(fileName);
|
||||
|
@ -937,9 +953,9 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
|
|||
}
|
||||
|
||||
abstract class UploadGroup implements Refresh {
|
||||
private Type type;
|
||||
private final Type type;
|
||||
private OCUpload[] items;
|
||||
private String name;
|
||||
private final String name;
|
||||
|
||||
UploadGroup(Type type, String groupName) {
|
||||
this.type = type;
|
||||
|
@ -956,6 +972,10 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
|
|||
}
|
||||
|
||||
public OCUpload getItem(int position) {
|
||||
if (items.length == 0 || position < 0 || position >= items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return items[position];
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import android.view.View
|
|||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.common.collect.Sets
|
||||
|
@ -40,8 +39,6 @@ import com.owncloud.android.ui.activity.FileDisplayActivity
|
|||
import com.owncloud.android.utils.DisplayUtils
|
||||
import com.owncloud.android.utils.KeyboardUtils
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -184,21 +181,18 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList
|
|||
}
|
||||
|
||||
val path = parentFolder?.decryptedRemotePath + newFolderName + OCFile.PATH_SEPARATOR
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
if (connectivityService.isNetworkAndServerAvailable()) {
|
||||
connectivityService.isNetworkAndServerAvailable { result ->
|
||||
if (result) {
|
||||
typedActivity<ComponentsGetter>()?.fileOperationsHelper?.createFolder(path)
|
||||
} else {
|
||||
Log_OC.d(TAG, "Network not available, creating offline operation")
|
||||
fileDataStorageManager.addCreateFolderOfflineOperation(
|
||||
path,
|
||||
newFolderName,
|
||||
parentFolder?.offlineOperationParentPath,
|
||||
parentFolder?.fileId
|
||||
)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
(requireActivity() as? FileDisplayActivity)?.syncAndUpdateFolder(true)
|
||||
}
|
||||
typedActivity<FileDisplayActivity>()?.refreshCurrentDirectory()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -146,7 +146,7 @@ class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListen
|
|||
}
|
||||
|
||||
if (mTargetFile?.isOfflineOperation == true) {
|
||||
fileDataStorageManager.renameCreateFolderOfflineOperation(mTargetFile, newFileName)
|
||||
fileDataStorageManager.renameOfflineOperation(mTargetFile, newFileName)
|
||||
if (requireActivity() is FileDisplayActivity) {
|
||||
val activity = requireActivity() as FileDisplayActivity
|
||||
activity.refreshCurrentDirectory()
|
||||
|
|
|
@ -45,6 +45,7 @@ import com.nextcloud.client.account.UserAccountManager;
|
|||
import com.nextcloud.client.di.Injectable;
|
||||
import com.nextcloud.client.preferences.AppPreferences;
|
||||
import com.nextcloud.client.preferences.AppPreferencesImpl;
|
||||
import com.nextcloud.utils.extensions.FragmentExtensionsKt;
|
||||
import com.owncloud.android.MainApp;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.databinding.ListFragmentBinding;
|
||||
|
@ -52,6 +53,7 @@ import com.owncloud.android.lib.common.utils.Log_OC;
|
|||
import com.owncloud.android.lib.resources.files.SearchRemoteOperation;
|
||||
import com.owncloud.android.lib.resources.status.OwnCloudVersion;
|
||||
import com.owncloud.android.ui.EmptyRecyclerView;
|
||||
import com.owncloud.android.ui.activity.FileActivity;
|
||||
import com.owncloud.android.ui.activity.FileDisplayActivity;
|
||||
import com.owncloud.android.ui.activity.FolderPickerActivity;
|
||||
import com.owncloud.android.ui.activity.OnEnforceableRefreshListener;
|
||||
|
@ -367,6 +369,10 @@ public class ExtendedListFragment extends Fragment implements
|
|||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
var adapter = getRecyclerView().getAdapter();
|
||||
if (adapter instanceof OCFileListAdapter ocFileListAdapter) {
|
||||
ocFileListAdapter.onDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
|
||||
|
@ -578,69 +584,67 @@ public class ExtendedListFragment extends Fragment implements
|
|||
*/
|
||||
public void setMessageForEmptyList(@StringRes final int headline, @StringRes final int message,
|
||||
@DrawableRes final int icon, final boolean tintIcon) {
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
new Handler(Looper.getMainLooper()).post(() -> {
|
||||
|
||||
if (mEmptyListContainer != null && mEmptyListMessage != null) {
|
||||
mEmptyListHeadline.setText(headline);
|
||||
mEmptyListMessage.setText(message);
|
||||
if (mEmptyListContainer != null && mEmptyListMessage != null) {
|
||||
mEmptyListHeadline.setText(headline);
|
||||
mEmptyListMessage.setText(message);
|
||||
|
||||
if (tintIcon) {
|
||||
if (getContext() != null) {
|
||||
mEmptyListIcon.setImageDrawable(
|
||||
viewThemeUtils.platform.tintPrimaryDrawable(getContext(), icon));
|
||||
}
|
||||
} else {
|
||||
mEmptyListIcon.setImageResource(icon);
|
||||
if (tintIcon) {
|
||||
if (getContext() != null) {
|
||||
mEmptyListIcon.setImageDrawable(
|
||||
viewThemeUtils.platform.tintPrimaryDrawable(getContext(), icon));
|
||||
}
|
||||
|
||||
mEmptyListIcon.setVisibility(View.VISIBLE);
|
||||
mEmptyListMessage.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mEmptyListIcon.setImageResource(icon);
|
||||
}
|
||||
|
||||
mEmptyListIcon.setVisibility(View.VISIBLE);
|
||||
mEmptyListMessage.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setEmptyListMessage(final SearchType searchType) {
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
if (searchType == SearchType.NO_SEARCH) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_headline,
|
||||
R.string.file_list_empty,
|
||||
R.drawable.ic_list_empty_folder,
|
||||
true);
|
||||
} else if (searchType == SearchType.FILE_SEARCH) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
|
||||
R.string.file_list_empty,
|
||||
R.drawable.ic_search_light_grey);
|
||||
} else if (searchType == SearchType.FAVORITE_SEARCH) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_favorite_headline,
|
||||
R.string.file_list_empty_favorites_filter_list,
|
||||
R.drawable.ic_star_light_yellow);
|
||||
} else if (searchType == SearchType.RECENTLY_MODIFIED_SEARCH) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
|
||||
R.string.file_list_empty_recently_modified,
|
||||
R.drawable.ic_list_empty_recent);
|
||||
} else if (searchType == SearchType.REGULAR_FILTER) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_headline_search,
|
||||
R.string.file_list_empty_search,
|
||||
R.drawable.ic_search_light_grey);
|
||||
} else if (searchType == SearchType.SHARED_FILTER) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_shared_headline,
|
||||
R.string.file_list_empty_shared,
|
||||
R.drawable.ic_list_empty_shared);
|
||||
} else if (searchType == SearchType.GALLERY_SEARCH) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
|
||||
R.string.file_list_empty_gallery,
|
||||
R.drawable.file_image);
|
||||
} else if (searchType == SearchType.LOCAL_SEARCH) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
|
||||
R.string.file_list_empty_local_search,
|
||||
R.drawable.ic_search_light_grey);
|
||||
}
|
||||
new Handler(Looper.getMainLooper()).post(() -> {
|
||||
if (searchType == SearchType.OFFLINE_MODE) {
|
||||
setMessageForEmptyList(R.string.offline_mode_info_title,
|
||||
R.string.offline_mode_info_description,
|
||||
R.drawable.ic_cloud_sync,
|
||||
true);
|
||||
} else if (searchType == SearchType.NO_SEARCH) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_headline,
|
||||
R.string.file_list_empty,
|
||||
R.drawable.ic_list_empty_folder,
|
||||
true);
|
||||
} else if (searchType == SearchType.FILE_SEARCH) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
|
||||
R.string.file_list_empty,
|
||||
R.drawable.ic_search_light_grey);
|
||||
} else if (searchType == SearchType.FAVORITE_SEARCH) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_favorite_headline,
|
||||
R.string.file_list_empty_favorites_filter_list,
|
||||
R.drawable.ic_star_light_yellow);
|
||||
} else if (searchType == SearchType.RECENTLY_MODIFIED_SEARCH) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
|
||||
R.string.file_list_empty_recently_modified,
|
||||
R.drawable.ic_list_empty_recent);
|
||||
} else if (searchType == SearchType.REGULAR_FILTER) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_headline_search,
|
||||
R.string.file_list_empty_search,
|
||||
R.drawable.ic_search_light_grey);
|
||||
} else if (searchType == SearchType.SHARED_FILTER) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_shared_headline,
|
||||
R.string.file_list_empty_shared,
|
||||
R.drawable.ic_list_empty_shared);
|
||||
} else if (searchType == SearchType.GALLERY_SEARCH) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
|
||||
R.string.file_list_empty_gallery,
|
||||
R.drawable.file_image);
|
||||
} else if (searchType == SearchType.LOCAL_SEARCH) {
|
||||
setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
|
||||
R.string.file_list_empty_local_search,
|
||||
R.drawable.ic_search_light_grey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -650,11 +654,15 @@ public class ExtendedListFragment extends Fragment implements
|
|||
*/
|
||||
public void setEmptyListLoadingMessage() {
|
||||
new Handler(Looper.getMainLooper()).post(() -> {
|
||||
if (mEmptyListContainer != null && mEmptyListMessage != null) {
|
||||
mEmptyListHeadline.setText(R.string.file_list_loading);
|
||||
mEmptyListMessage.setText("");
|
||||
FileActivity fileActivity = FragmentExtensionsKt.getTypedActivity(this, FileActivity.class);
|
||||
if (fileActivity != null) {
|
||||
fileActivity.connectivityService.isNetworkAndServerAvailable(result -> {
|
||||
if (!result || mEmptyListContainer == null || mEmptyListMessage == null) return;
|
||||
|
||||
mEmptyListIcon.setVisibility(View.GONE);
|
||||
mEmptyListHeadline.setText(R.string.file_list_loading);
|
||||
mEmptyListMessage.setText("");
|
||||
mEmptyListIcon.setVisibility(View.GONE);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -351,11 +351,12 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
|
|||
}
|
||||
});
|
||||
|
||||
binding.tabLayout.post(() -> {
|
||||
TabLayout.Tab tab1 = binding.tabLayout.getTabAt(activeTab);
|
||||
if (tab1 == null) return;
|
||||
tab1.select();
|
||||
});
|
||||
// FIXME file detail not opening from Media tab
|
||||
if (binding != null) {
|
||||
TabLayout.Tab tab = binding.tabLayout.getTabAt(activeTab);
|
||||
if (tab == null) return;
|
||||
binding.tabLayout.selectTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -580,8 +581,9 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
|
|||
}
|
||||
|
||||
setupViewPager();
|
||||
|
||||
getView().invalidate();
|
||||
if (getView() != null) {
|
||||
getView().invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void setFileModificationTimestamp(OCFile file, boolean showDetailedTimestamp) {
|
||||
|
|
|
@ -346,7 +346,9 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
|
|||
@Override
|
||||
@VisibleForTesting
|
||||
public void showSharingMenuActionSheet(OCShare share) {
|
||||
new FileDetailSharingMenuBottomSheetDialog(fileActivity, this, share, viewThemeUtils).show();
|
||||
if (fileActivity != null && !fileActivity.isFinishing()) {
|
||||
new FileDetailSharingMenuBottomSheetDialog(fileActivity, this, share, viewThemeUtils).show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -214,19 +214,22 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog implements In
|
|||
private void filterActionsForOfflineOperations() {
|
||||
if (file == null) return;
|
||||
|
||||
if (!file.isOfflineOperation() || file.isRootDirectory()) {
|
||||
return;
|
||||
}
|
||||
fileActivity.connectivityService.isNetworkAndServerAvailable(result -> {
|
||||
if (file.isRootDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
binding.menuCreateRichWorkspace.setVisibility(View.GONE);
|
||||
binding.menuUploadFromApp.setVisibility(View.GONE);
|
||||
binding.menuDirectCameraUpload.setVisibility(View.GONE);
|
||||
binding.menuScanDocUpload.setVisibility(View.GONE);
|
||||
binding.menuUploadFiles.setVisibility(View.GONE);
|
||||
binding.menuNewDocument.setVisibility(View.GONE);
|
||||
binding.menuNewSpreadsheet.setVisibility(View.GONE);
|
||||
binding.menuNewPresentation.setVisibility(View.GONE);
|
||||
binding.creatorsContainer.setVisibility(View.GONE);
|
||||
if (!result || file.isOfflineOperation()) {
|
||||
binding.menuCreateRichWorkspace.setVisibility(View.GONE);
|
||||
binding.menuUploadFromApp.setVisibility(View.GONE);
|
||||
binding.menuDirectCameraUpload.setVisibility(View.GONE);
|
||||
binding.menuScanDocUpload.setVisibility(View.GONE);
|
||||
binding.menuNewDocument.setVisibility(View.GONE);
|
||||
binding.menuNewSpreadsheet.setVisibility(View.GONE);
|
||||
binding.menuNewPresentation.setVisibility(View.GONE);
|
||||
binding.creatorsContainer.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -638,7 +638,16 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
|||
for (OCFile file : checkedFiles) {
|
||||
if (file.isOfflineOperation()) {
|
||||
toHide = new ArrayList<>(
|
||||
Arrays.asList(R.id.action_favorite, R.id.action_move_or_copy, R.id.action_sync_file, R.id.action_encrypted, R.id.action_unset_encrypted)
|
||||
Arrays.asList(R.id.action_favorite,
|
||||
R.id.action_move_or_copy,
|
||||
R.id.action_sync_file,
|
||||
R.id.action_encrypted,
|
||||
R.id.action_unset_encrypted,
|
||||
R.id.action_edit,
|
||||
R.id.action_download_file,
|
||||
R.id.action_export_file,
|
||||
R.id.action_set_as_wallpaper
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
@ -1129,10 +1138,16 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
|||
Log_OC.d(TAG, "no public key for " + user.getAccountName());
|
||||
|
||||
FragmentManager fragmentManager = getParentFragmentManager();
|
||||
if (fragmentManager.findFragmentByTag(SETUP_ENCRYPTION_DIALOG_TAG) == null) {
|
||||
SetupEncryptionDialogFragment dialog = SetupEncryptionDialogFragment.newInstance(user, position);
|
||||
dialog.setTargetFragment(this, SETUP_ENCRYPTION_REQUEST_CODE);
|
||||
dialog.show(fragmentManager, SETUP_ENCRYPTION_DIALOG_TAG);
|
||||
if (fragmentManager.findFragmentByTag(SETUP_ENCRYPTION_DIALOG_TAG) == null && requireActivity() instanceof FileActivity fileActivity) {
|
||||
fileActivity.connectivityService.isNetworkAndServerAvailable(result -> {
|
||||
if (result) {
|
||||
SetupEncryptionDialogFragment dialog = SetupEncryptionDialogFragment.newInstance(user, position);
|
||||
dialog.setTargetFragment(this, SETUP_ENCRYPTION_REQUEST_CODE);
|
||||
dialog.show(fragmentManager, SETUP_ENCRYPTION_DIALOG_TAG);
|
||||
} else {
|
||||
DisplayUtils.showSnackMessage(fileActivity, R.string.internet_connection_required_for_encrypted_folder_setup);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -1143,13 +1158,25 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
|||
}
|
||||
}
|
||||
|
||||
private void fileOnItemClick(OCFile file) {
|
||||
private Integer checkFileBeforeOpen(OCFile file) {
|
||||
if (isAPKorAAB(Set.of(file))) {
|
||||
return R.string.gplay_restriction;
|
||||
} else if (file.isOfflineOperation()) {
|
||||
return R.string.offline_operations_file_does_not_exists_yet;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void fileOnItemClick(OCFile file) {
|
||||
Integer errorMessageId = checkFileBeforeOpen(file);
|
||||
if (errorMessageId != null) {
|
||||
Snackbar.make(getRecyclerView(),
|
||||
R.string.gplay_restriction,
|
||||
errorMessageId,
|
||||
Snackbar.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (PreviewImageFragment.canBePreviewed(file)) {
|
||||
// preview image - it handles the download, if needed
|
||||
if (searchFragment) {
|
||||
|
|
|
@ -22,5 +22,6 @@ enum class SearchType : Parcelable {
|
|||
|
||||
// not a real filter, but nevertheless
|
||||
SHARED_FILTER,
|
||||
GROUPFOLDER
|
||||
GROUPFOLDER,
|
||||
OFFLINE_MODE
|
||||
}
|
||||
|
|
|
@ -57,6 +57,26 @@ public final class BitmapUtils {
|
|||
// utility class -> private constructor
|
||||
}
|
||||
|
||||
public static Bitmap addColorFilter(Bitmap originalBitmap, int filterColor, int opacity) {
|
||||
int width = originalBitmap.getWidth();
|
||||
int height = originalBitmap.getHeight();
|
||||
|
||||
Bitmap resultBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(resultBitmap);
|
||||
|
||||
canvas.drawBitmap(originalBitmap, 0, 0, null);
|
||||
|
||||
Paint paint = new Paint();
|
||||
paint.setColor(filterColor);
|
||||
|
||||
paint.setAlpha(opacity);
|
||||
|
||||
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
|
||||
canvas.drawRect(0, 0, width, height, paint);
|
||||
|
||||
return resultBitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a bitmap from a file containing it minimizing the memory use, known that the bitmap will be drawn in a
|
||||
* surface of reqWidth x reqHeight
|
||||
|
|
18
app/src/main/res/drawable/ic_cloud_sync.xml
Normal file
18
app/src/main/res/drawable/ic_cloud_sync.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<!--
|
||||
~ Nextcloud - Android Client
|
||||
~
|
||||
~ SPDX-FileCopyrightText: 2018-2024 Google LLC
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21.5,14.98c-0.02,0 -0.03,0 -0.05,0.01C21.2,13.3 19.76,12 18,12c-1.4,0 -2.6,0.83 -3.16,2.02C13.26,14.1 12,15.4 12,17c0,1.66 1.34,3 3,3l6.5,-0.02c1.38,0 2.5,-1.12 2.5,-2.5S22.88,14.98 21.5,14.98zM10,4.26v2.09C7.67,7.18 6,9.39 6,12c0,1.77 0.78,3.34 2,4.44V14h2v6H4v-2h2.73C5.06,16.54 4,14.4 4,12C4,8.27 6.55,5.15 10,4.26zM20,6h-2.73c1.43,1.26 2.41,3.01 2.66,5l-2.02,0C17.68,9.64 16.98,8.45 16,7.56V10h-2V4h6V6z" />
|
||||
|
||||
</vector>
|
|
@ -13,7 +13,9 @@
|
|||
<string name="action_send_share">Siųsti/Bendrinti</string>
|
||||
<string name="action_switch_grid_view">Tinklelio rodinys</string>
|
||||
<string name="action_switch_list_view">Sąrašo rodinys</string>
|
||||
<string name="actionbar_calendar_contacts_restore">Atkurti adresatus ir kalendorių</string>
|
||||
<string name="actionbar_mkdir">Naujas aplankas</string>
|
||||
<string name="actionbar_move_or_copy">Perkelti ar kopijuoti</string>
|
||||
<string name="actionbar_open_with">Atverti naudojant</string>
|
||||
<string name="actionbar_search">Paieška</string>
|
||||
<string name="actionbar_see_details">Išsamiau</string>
|
||||
|
@ -32,10 +34,20 @@
|
|||
<string name="advanced_settings">Išplėstiniai nustatymai</string>
|
||||
<string name="allow_resharing">Leisti bendrinti iš naujo</string>
|
||||
<string name="app_config_proxy_port_title">Įgaliotojo serverio prievadas</string>
|
||||
<string name="app_widget_description">Rodo vieną valdiklį iš skydelio</string>
|
||||
<string name="appbar_search_in">Ieškoti %s</string>
|
||||
<string name="assistant_screen_all_task_type">Visos</string>
|
||||
<string name="assistant_screen_create_task_alert_dialog_input_field_placeholder">Įrašykite kokį nors tekstą</string>
|
||||
<string name="assistant_screen_delete_task_alert_dialog_description">Ar tikrai norite ištrinti šią užduotį?</string>
|
||||
<string name="assistant_screen_delete_task_alert_dialog_title">Ištrinti užduotį</string>
|
||||
<string name="assistant_screen_failed_task_text">Nepavyko</string>
|
||||
<string name="assistant_screen_successful_task_text">Užbaigta</string>
|
||||
<string name="assistant_screen_task_create_fail_message">Kuriant užduotį, įvyko klaida</string>
|
||||
<string name="assistant_screen_task_create_success_message">Užduotis sėkmingai sukurta</string>
|
||||
<string name="assistant_screen_task_delete_fail_message">Ištrinant užduotį, įvyko klaida</string>
|
||||
<string name="assistant_screen_task_delete_success_message">Užduotis sėkmingai ištrinta</string>
|
||||
<string name="assistant_screen_task_list_error_state_message">Nepavyko gauti užduočių sąrašo, patikrinkite savo interneto ryšį.</string>
|
||||
<string name="assistant_screen_task_more_actions_bottom_sheet_delete_action">Ištrinti užduotį</string>
|
||||
<string name="assistant_screen_unknown_task_status_text">Nežinoma</string>
|
||||
<string name="associated_account_not_found">Susieta paskyra nerasta!</string>
|
||||
<string name="auth_access_failed">Prieiga nepavyko: %1$s</string>
|
||||
|
@ -195,6 +207,7 @@
|
|||
<string name="dismiss">Atmesti</string>
|
||||
<string name="dismiss_notification_description">Atmesti pranešimą</string>
|
||||
<string name="dnd">Netrukdyti</string>
|
||||
<string name="document_scan_export_dialog_pdf">PDF failas</string>
|
||||
<string name="done">Atlikta</string>
|
||||
<string name="dontClear">Neišvalyti</string>
|
||||
<string name="download_cannot_create_file">Nepavyksta sukurti vietinį failą</string>
|
||||
|
@ -203,11 +216,14 @@
|
|||
<string name="downloader_download_failed_credentials_error">Atsiuntimas nepavyko, prisijunkite dar kartą</string>
|
||||
<string name="downloader_download_failed_ticker">Atsiuntimas nepavyko</string>
|
||||
<string name="downloader_download_file_not_found">Failas neegzistuoja serveryje</string>
|
||||
<string name="downloader_download_in_progress">%1$d%% %2$s</string>
|
||||
<string name="downloader_download_in_progress_content">%1$d%% Atsiunčiama %2$s</string>
|
||||
<string name="downloader_download_in_progress_ticker">Atsisiunčiama…</string>
|
||||
<string name="downloader_download_succeeded_content">%1$s atsisiųsta</string>
|
||||
<string name="downloader_download_succeeded_ticker">Atsisiųsti</string>
|
||||
<string name="downloader_file_download_failed">Atsiunčiant failus, įvyko klaida</string>
|
||||
<string name="downloader_not_downloaded_yet">Kol kas neatsisiųsta</string>
|
||||
<string name="downloader_unexpected_error">Atsiunčiant failus, įvyko netikėta klaida</string>
|
||||
<string name="drawer_close">Užverti šoninę juostą</string>
|
||||
<string name="drawer_community">Bendruomenė</string>
|
||||
<string name="drawer_header_background">Stalčiaus antraštės foninis paveikslas</string>
|
||||
|
@ -218,6 +234,7 @@
|
|||
<string name="drawer_item_home">Namai</string>
|
||||
<string name="drawer_item_notifications">Pranešimai</string>
|
||||
<string name="drawer_item_on_device">Įrenginyje</string>
|
||||
<string name="drawer_item_personal_files">Asmeniniai failai</string>
|
||||
<string name="drawer_item_recently_modified">Paskiausiai modifikuoti</string>
|
||||
<string name="drawer_item_shared">Bendrinami</string>
|
||||
<string name="drawer_item_trashbin">Ištrinti failai</string>
|
||||
|
@ -227,10 +244,14 @@
|
|||
<string name="drawer_quota">panaudota %1$s iš %2$s</string>
|
||||
<string name="drawer_quota_unlimited">panaudota %1$s</string>
|
||||
<string name="drawer_synced_folders">Automatinis įkėlimas</string>
|
||||
<string name="e2e_hash_not_found">Maiša nerasta</string>
|
||||
<string name="e2e_offline">Neįmanoma, kai nėra interneto ryšio</string>
|
||||
<string name="e2e_signature_does_not_match">Parašas nesutampa</string>
|
||||
<string name="ecosystem_apps_display_more">Daugiau</string>
|
||||
<string name="ecosystem_apps_display_notes">Užrašai</string>
|
||||
<string name="ecosystem_apps_display_talk">Kalba</string>
|
||||
<string name="ecosystem_apps_notes">Nextcloud užrašai</string>
|
||||
<string name="email_pick_failed">Nepavyko pasirinkti el. pašto adreso.</string>
|
||||
<string name="encrypted">Nustatyti kaip šifruotą</string>
|
||||
<string name="end_to_end_encryption_confirm_button">Nustatyti šifravimą</string>
|
||||
<string name="end_to_end_encryption_decrypting">Iššifruojama…</string>
|
||||
|
@ -334,6 +355,10 @@
|
|||
<string name="file_migration_updating_index">Atnaujinamas indeksas…</string>
|
||||
<string name="file_migration_use_data_folder">Naudok</string>
|
||||
<string name="file_migration_waiting_for_unfinished_sync">Laukiama pilno sinchronizavimo…</string>
|
||||
<string name="file_name_validator_error_forbidden_space_character_extensions">Failų pavadinimų pradžioje ar pabaigoje negali būti tarpų</string>
|
||||
<string name="file_name_validator_error_invalid_character">Pavadinime yra netinkamų simbolių: %s</string>
|
||||
<string name="file_name_validator_error_reserved_names">%s yra draudžiamas pavadinimas</string>
|
||||
<string name="file_name_validator_rename_before_move_or_copy">%s. Prieš perkeldami ar kopijuodami, pervadinkite failą</string>
|
||||
<string name="file_not_found">Failas nerastas</string>
|
||||
<string name="file_not_synced">Nepavyko sinchronizuoti failo. Rodoma naujausia galima versija.
|
||||
</string>
|
||||
|
@ -376,6 +401,15 @@
|
|||
<string name="hint_password">Slaptažodis</string>
|
||||
<string name="host_not_available">Serveris neprieinamas</string>
|
||||
<string name="host_your_own_server">Administruoti savo serverį</string>
|
||||
<string name="image_editor_flip_horizontal">Apversti horizontaliai</string>
|
||||
<string name="image_editor_flip_vertical">Apversti vertikaliai</string>
|
||||
<string name="image_preview_filedetails">Išsamiau apie failą</string>
|
||||
<string name="image_preview_image_taking_conditions">Fotografavimo sąlygos</string>
|
||||
<string name="image_preview_unit_fnumber">ƒ/%s</string>
|
||||
<string name="image_preview_unit_iso">ISO %s</string>
|
||||
<string name="image_preview_unit_megapixel">%s MP</string>
|
||||
<string name="image_preview_unit_millimetres">%s mm</string>
|
||||
<string name="image_preview_unit_seconds">%s sek.</string>
|
||||
<string name="in_folder">aplanke %1$s</string>
|
||||
<string name="instant_upload_existing">Taip pat įkelkite esamus failus</string>
|
||||
<string name="instant_upload_on_charging">Įkelti failus tik kai kraunasi</string>
|
||||
|
@ -467,6 +501,7 @@
|
|||
<string name="notification_channel_upload_description">Rodo įkėlimo eigą</string>
|
||||
<string name="notification_channel_upload_name_short">Įkėlimai</string>
|
||||
<string name="notification_icon">Pranešimo piktograma</string>
|
||||
<string name="notification_icon_description">Yra neskaitytų pranešimų</string>
|
||||
<string name="notifications_no_results_headline">Pranešimų nėra</string>
|
||||
<string name="notifications_no_results_message">Prašome patikrinti vėliau.</string>
|
||||
<string name="offline_mode">Nėra interneto ryšio</string>
|
||||
|
@ -488,12 +523,15 @@
|
|||
<string name="permission_deny">Drausti</string>
|
||||
<string name="permission_storage_access">Papildomos teisės reikalingos įkelti šiuos atsisiųstus failus.</string>
|
||||
<string name="picture_set_as_no_app">Nerasta programa, su kuria būtų galima nustatyti nuotrauką</string>
|
||||
<string name="pin_shortcut_label">Atverti %1$s</string>
|
||||
<string name="placeholder_fileSize">389 KB</string>
|
||||
<string name="placeholder_filename">rezervas.txt</string>
|
||||
<string name="placeholder_media_time">12:23:45</string>
|
||||
<string name="placeholder_sentence">Rezervas</string>
|
||||
<string name="placeholder_timestamp">2012/05/18 12:23 PM</string>
|
||||
<string name="player_stop">stabdyti</string>
|
||||
<string name="player_toggle">perjungti</string>
|
||||
<string name="please_select_a_server">Pasirinkite serverį…</string>
|
||||
<string name="power_save_check_dialog_message">Išjungtas energijos taupymo tikrinimas gali įtakoti failų įkėlimą, kai akumuliatoriaus energija yra maža!</string>
|
||||
<string name="pref_behaviour_entries_delete_file">Ištrintas</string>
|
||||
<string name="pref_behaviour_entries_keep_file">paliktas pradiniame aplanke</string>
|
||||
|
@ -505,6 +543,7 @@
|
|||
<string name="pref_instant_name_collision_policy_entries_rename">Pervadinti naują versiją</string>
|
||||
<string name="pref_instant_name_collision_policy_title">Ką daryti, jei failas jau yra?</string>
|
||||
<string name="prefs_add_account">Pridėti paskyrą</string>
|
||||
<string name="prefs_calendar_contacts">Sinchronizuoti kalendorių ir adresatus</string>
|
||||
<string name="prefs_calendar_contacts_no_store_error">Nėra įdiegta nei F-Droid, nei Google Play</string>
|
||||
<string name="prefs_calendar_contacts_summary">Dabartinei paskyrai nustatykite „DAVx5“ (anksčiau žinomą kaip „DAVdroid“) (v1.3.0 +)</string>
|
||||
<string name="prefs_category_about">Apie</string>
|
||||
|
@ -561,6 +600,8 @@
|
|||
<string name="remote">(nuotolinis)</string>
|
||||
<string name="remote_file_fetch_failed">Nepavyko rasti failo!</string>
|
||||
<string name="remove_fail_msg">Ištrynimas nepavyko</string>
|
||||
<string name="remove_local_account">Šalinti vietinę paskyrą</string>
|
||||
<string name="remove_local_account_details">Šalinti paskyrą iš įrenginio ir ištrinti visus vietinius failus</string>
|
||||
<string name="remove_notification_failed">Nepavyko pašalinti pranešimo.</string>
|
||||
<string name="remove_push_notification">Šalinti</string>
|
||||
<string name="remove_success_msg">Ištrinta</string>
|
||||
|
@ -588,6 +629,7 @@
|
|||
<string name="screenshot_04_accounts_heading">Visos jūsų paskyros</string>
|
||||
<string name="screenshot_04_accounts_subline">vienoje vietoje</string>
|
||||
<string name="screenshot_05_autoUpload_heading">Automatinis įkėlimas</string>
|
||||
<string name="screenshot_06_davdroid_heading">Kalendorius ir adresatai</string>
|
||||
<string name="screenshot_06_davdroid_subline">Sinchronizacija su DAVx5</string>
|
||||
<string name="search_error">Klaida gaunant paieškos rezultatus</string>
|
||||
<string name="select_all">Pažymėti viską</string>
|
||||
|
@ -640,6 +682,8 @@
|
|||
<string name="shared_icon_shared_via_link">bendrinta per nuorodą</string>
|
||||
<string name="shared_with_you_by">%1$s bendrina su Jumis.</string>
|
||||
<string name="sharee_add_failed">Bendrinimo pridėjimas nepavyko</string>
|
||||
<string name="show_images">Rodyti nuotraukas</string>
|
||||
<string name="show_video">Rodyti vaizdo įrašus</string>
|
||||
<string name="signup_with_provider">Registruotis su tiekėju</string>
|
||||
<string name="single_sign_on_request_token" formatted="true">Leisti %1$s gauti prieigą prie jūsų Nextcloud paskyros %2$s?</string>
|
||||
<string name="sort_by">Rikiuoti pagal</string>
|
||||
|
@ -792,6 +836,7 @@
|
|||
<string name="uploader_upload_files_behaviour_only_upload">Laikyti failą pagrindiniame (šaltinio) aplanke</string>
|
||||
<string name="uploader_upload_files_behaviour_upload_and_delete_from_source">Ištrinkite failus iš pagrindinio aplanko</string>
|
||||
<string name="uploader_upload_forbidden_permissions">Galite įkelti į šį aplanką</string>
|
||||
<string name="uploader_upload_in_progress">%1$d%% %2$s</string>
|
||||
<string name="uploader_upload_in_progress_content">%1$d%% Siunčiama %2$s</string>
|
||||
<string name="uploader_upload_in_progress_ticker">Įkeliama…</string>
|
||||
<string name="uploader_upload_succeeded_content_single">%1$s įkelta</string>
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
<string name="app_config_base_url_title">Base URL</string>
|
||||
<string name="app_config_proxy_host_title">Proxy Host Name</string>
|
||||
<string name="app_config_proxy_port_title">Proxy Port</string>
|
||||
<string name="offline_operations_file_does_not_exists_yet">File does not exists, yet. Please upload the file first.</string>
|
||||
<string name="offline_operations_worker_notification_delete_offline_folder">Delete Offline Folder</string>
|
||||
<string name="offline_operations_worker_notification_conflict_text">Conflicted Folder: %s</string>
|
||||
<string name="offline_operations_worker_notification_start_text">Starting Offline Operations</string>
|
||||
|
@ -609,7 +610,8 @@
|
|||
<string name="confirmation_remove_folders_alert">Do you really want to delete the selected items and their contents?</string>
|
||||
<string name="maintenance_mode">Server is in maintenance mode</string>
|
||||
<string name="offline_mode">No internet connection</string>
|
||||
|
||||
<string name="offline_mode_info_title">You\'re Offline, But Work Continues</string>
|
||||
<string name="offline_mode_info_description">Even without an internet connection, you can organize your folders, create files. Once you\'re back online, your pending actions will automatically sync.</string>
|
||||
<string name="uploads_view_upload_status_waiting_for_charging">Awaiting charge</string>
|
||||
<string name="actionbar_search">Search</string>
|
||||
<string name="drawer_synced_folders">Auto upload</string>
|
||||
|
@ -1175,6 +1177,7 @@
|
|||
<string name="pin_home">Pin to Home screen</string>
|
||||
<string name="pin_shortcut_label">Open %1$s</string>
|
||||
<string name="displays_mnemonic">Displays your 12 word passphrase</string>
|
||||
<string name="internet_connection_required_for_encrypted_folder_setup">An internet connection is required to set up the encrypted folder</string>
|
||||
<string name="prefs_setup_e2e">Set up end-to-end encryption</string>
|
||||
<string name="prefs_e2e_active">End-to-end encryption is set up!</string>
|
||||
<string name="prefs_remove_e2e">Remove encryption locally</string>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<ignored-key id="0AA3E5C3D232E79B" reason="Key couldn't be downloaded from any key server"/>
|
||||
<ignored-key id="23778689FBFBE047" reason="Key couldn't be downloaded from any key server"/>
|
||||
<ignored-key id="31D2D79DF7E85DD3" reason="Key couldn't be downloaded from any key server"/>
|
||||
<ignored-key id="5C504E1210E49773" reason="Key couldn't be downloaded from any key server"/>
|
||||
</ignored-keys>
|
||||
<trusted-keys>
|
||||
<trusted-key id="015479E1055341431B4545AB72475FD306B9CAB7" group="com.googlecode.javaewah" name="JavaEWAH" version="1.2.3"/>
|
||||
|
@ -8055,6 +8056,14 @@
|
|||
<sha256 value="1f3b5a26459843c6107ce4b904e2325a93c2091c2d0005fec272454fc9adaa24" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.mebigfatguy.fb-contrib" name="fb-contrib" version="7.6.5">
|
||||
<artifact name="fb-contrib-7.6.5.jar">
|
||||
<sha256 value="122f28fd26ed6c1a9bad1e1a028198f47c1c057054103849323376224932038d" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
|
||||
</artifact>
|
||||
<artifact name="fb-contrib-7.6.5.pom">
|
||||
<sha256 value="eb308c03982ff98baf9edbf071c4e8796146686eaba0f28eaf2b67516c6a0113" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.puppycrawl.tools" name="checkstyle" version="9.3">
|
||||
<artifact name="checkstyle-9.3.jar">
|
||||
<sha256 value="0463e304980f5460b964f481ccc25a10fb253b60100c19e50fc992892895977f" origin="Generated by Gradle"/>
|
||||
|
|
Loading…
Reference in a new issue