Merge remote-tracking branch 'origin/master' into dev

This commit is contained in:
Tobias Kaminsky 2024-10-10 02:34:11 +02:00
commit d94dbe85fc
48 changed files with 2132 additions and 323 deletions

View file

@ -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"

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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());
}
}

View file

@ -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)
}
}
}
}
}

View file

@ -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?) {

View file

@ -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())

View file

@ -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()
}

View file

@ -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
)

View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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)

View file

@ -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)
}
}
}
}

View file

@ -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>
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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,

View file

@ -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);

View file

@ -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;
}

View file

@ -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";

View file

@ -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);
}

View file

@ -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 {

View file

@ -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;

View file

@ -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,

View file

@ -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> {

View file

@ -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];
}

View file

@ -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()
}
}
}

View file

@ -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()

View file

@ -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);
});
}
});
}

View file

@ -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) {

View file

@ -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();
}
}
/**

View file

@ -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

View file

@ -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) {

View file

@ -22,5 +22,6 @@ enum class SearchType : Parcelable {
// not a real filter, but nevertheless
SHARED_FILTER,
GROUPFOLDER
GROUPFOLDER,
OFFLINE_MODE
}

View file

@ -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

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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"/>