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

This commit is contained in:
Tobias Kaminsky 2024-03-26 03:40:12 +01:00
commit caa54f762e
19 changed files with 297 additions and 239 deletions

View file

@ -8,7 +8,7 @@ buildscript {
classpath "com.android.tools.build:gradle:$androidPluginVersion" classpath "com.android.tools.build:gradle:$androidPluginVersion"
classpath 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.9' classpath 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.9'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.5" classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.6"
classpath "commons-httpclient:commons-httpclient:3.1@jar" // remove after entire switch to lib v2 classpath "commons-httpclient:commons-httpclient:3.1@jar" // remove after entire switch to lib v2
classpath 'com.karumi:shot:6.1.0' classpath 'com.karumi:shot:6.1.0'
classpath "org.jacoco:org.jacoco.core:$jacoco_version" classpath "org.jacoco:org.jacoco.core:$jacoco_version"

View file

@ -214,7 +214,7 @@ class BackgroundJobManagerTest {
fun job_is_unique_and_replaces_previous_job() { fun job_is_unique_and_replaces_previous_job() {
verify(workManager).enqueueUniqueWork( verify(workManager).enqueueUniqueWork(
eq(BackgroundJobManagerImpl.JOB_CONTENT_OBSERVER), eq(BackgroundJobManagerImpl.JOB_CONTENT_OBSERVER),
eq(ExistingWorkPolicy.REPLACE), eq(ExistingWorkPolicy.APPEND),
argThat(IsOneTimeWorkRequest()) argThat(IsOneTimeWorkRequest())
) )
} }

View file

@ -36,24 +36,24 @@ import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1; import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedMetadata; import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedMetadata;
import com.owncloud.android.datamodel.e2e.v1.decrypted.Encrypted; import com.owncloud.android.datamodel.e2e.v1.decrypted.Encrypted;
import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFile;
import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1; import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1;
import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.lib.resources.e2ee.CsrHelper; import com.owncloud.android.lib.resources.e2ee.CsrHelper;
import com.owncloud.android.utils.EncryptionUtils; import com.owncloud.android.utils.EncryptionUtils;
import org.apache.commons.codec.binary.Hex; import org.junit.Assert;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.security.DigestInputStream;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.KeyPairGenerator; import java.security.KeyPairGenerator;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.interfaces.RSAPrivateCrtKey; import java.security.interfaces.RSAPrivateCrtKey;
@ -66,6 +66,7 @@ import java.util.Random;
import java.util.Set; import java.util.Set;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import static com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes; import static com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes;
import static com.owncloud.android.utils.EncryptionUtils.decryptFile; import static com.owncloud.android.utils.EncryptionUtils.decryptFile;
@ -75,7 +76,6 @@ import static com.owncloud.android.utils.EncryptionUtils.decryptStringAsymmetric
import static com.owncloud.android.utils.EncryptionUtils.decryptStringSymmetric; import static com.owncloud.android.utils.EncryptionUtils.decryptStringSymmetric;
import static com.owncloud.android.utils.EncryptionUtils.deserializeJSON; import static com.owncloud.android.utils.EncryptionUtils.deserializeJSON;
import static com.owncloud.android.utils.EncryptionUtils.encodeBytesToBase64String; import static com.owncloud.android.utils.EncryptionUtils.encodeBytesToBase64String;
import static com.owncloud.android.utils.EncryptionUtils.encryptFile;
import static com.owncloud.android.utils.EncryptionUtils.encryptFolderMetadata; import static com.owncloud.android.utils.EncryptionUtils.encryptFolderMetadata;
import static com.owncloud.android.utils.EncryptionUtils.generateChecksum; import static com.owncloud.android.utils.EncryptionUtils.generateChecksum;
import static com.owncloud.android.utils.EncryptionUtils.generateKey; import static com.owncloud.android.utils.EncryptionUtils.generateKey;
@ -99,6 +99,11 @@ public class EncryptionTestIT extends AbstractIT {
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext); ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
private static final String MD5_ALGORITHM = "MD5";
private static final String filename = "ia7OEEEyXMoRa1QWQk8r";
private static final String secondFilename = "n9WXAIXO2wRY4R8nXwmo";
public static final String privateKey = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAo" + public static final String privateKey = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAo" +
"IBAQDsn0JKS/THu328z1IgN0VzYU53HjSX03WJIgWkmyTaxbiKpoJaKbksXmfSpgzV" + "IBAQDsn0JKS/THu328z1IgN0VzYU53HjSX03WJIgWkmyTaxbiKpoJaKbksXmfSpgzV" +
"GzKFvGfZ03fwFrN7Q8P8R2e8SNiell7mh1TDw9/0P7Bt/ER8PJrXORo+GviKHxaLr7" + "GzKFvGfZ03fwFrN7Q8P8R2e8SNiell7mh1TDw9/0P7Bt/ER8PJrXORo+GviKHxaLr7" +
@ -395,34 +400,27 @@ public class EncryptionTestIT extends AbstractIT {
public void testCryptFileWithoutMetadata() throws Exception { public void testCryptFileWithoutMetadata() throws Exception {
byte[] key = decodeStringToBase64Bytes("WANM0gRv+DhaexIsI0T3Lg=="); byte[] key = decodeStringToBase64Bytes("WANM0gRv+DhaexIsI0T3Lg==");
byte[] iv = decodeStringToBase64Bytes("gKm3n+mJzeY26q4OfuZEqg=="); byte[] iv = decodeStringToBase64Bytes("gKm3n+mJzeY26q4OfuZEqg==");
byte[] authTag = decodeStringToBase64Bytes("PboI9tqHHX3QeAA22PIu4w==");
assertTrue(cryptFile("ia7OEEEyXMoRa1QWQk8r", "78f42172166f9dc8fd1a7156b1753353", key, iv, authTag)); assertTrue(cryptFile(filename, "78f42172166f9dc8fd1a7156b1753353", key, iv));
} }
@Test @Test
public void cryptFileWithMetadata() throws Exception { public void cryptFileWithMetadata() throws Exception {
DecryptedFolderMetadataFileV1 metadata = generateFolderMetadataV1_1(); DecryptedFolderMetadataFileV1 metadata = generateFolderMetadataV1_1();
// n9WXAIXO2wRY4R8nXwmo assertTrue(cryptFile(filename,
assertTrue(cryptFile("ia7OEEEyXMoRa1QWQk8r",
"78f42172166f9dc8fd1a7156b1753353", "78f42172166f9dc8fd1a7156b1753353",
decodeStringToBase64Bytes(metadata.getFiles().get("ia7OEEEyXMoRa1QWQk8r") decodeStringToBase64Bytes(metadata.getFiles().get(filename)
.getEncrypted().getKey()), .getEncrypted().getKey()),
decodeStringToBase64Bytes(metadata.getFiles().get("ia7OEEEyXMoRa1QWQk8r") decodeStringToBase64Bytes(metadata.getFiles().get(filename)
.getInitializationVector()), .getInitializationVector())));
decodeStringToBase64Bytes(metadata.getFiles().get("ia7OEEEyXMoRa1QWQk8r")
.getAuthenticationTag())));
// n9WXAIXO2wRY4R8nXwmo assertTrue(cryptFile(secondFilename,
assertTrue(cryptFile("n9WXAIXO2wRY4R8nXwmo",
"825143ed1f21ebb0c3b3c3f005b2f5db", "825143ed1f21ebb0c3b3c3f005b2f5db",
decodeStringToBase64Bytes(metadata.getFiles().get("n9WXAIXO2wRY4R8nXwmo") decodeStringToBase64Bytes(metadata.getFiles().get(secondFilename)
.getEncrypted().getKey()), .getEncrypted().getKey()),
decodeStringToBase64Bytes(metadata.getFiles().get("n9WXAIXO2wRY4R8nXwmo") decodeStringToBase64Bytes(metadata.getFiles().get(secondFilename)
.getInitializationVector()), .getInitializationVector())));
decodeStringToBase64Bytes(metadata.getFiles().get("n9WXAIXO2wRY4R8nXwmo")
.getAuthenticationTag())));
} }
@Test @Test
@ -738,8 +736,8 @@ public class EncryptionTestIT extends AbstractIT {
DecryptedFolderMetadataFileV1 metadata = new DecryptedFolderMetadataFileV1(); DecryptedFolderMetadataFileV1 metadata = new DecryptedFolderMetadataFileV1();
String mnemonic = "chimney potato joke science ridge trophy result estate spare vapor much room"; String mnemonic = "chimney potato joke science ridge trophy result estate spare vapor much room";
metadata.getFiles().put("n9WXAIXO2wRY4R8nXwmo", new DecryptedFile()); metadata.getFiles().put(secondFilename, new DecryptedFile());
metadata.getFiles().put("ia7OEEEyXMoRa1QWQk8r", new DecryptedFile()); metadata.getFiles().put(filename, new DecryptedFile());
String encryptedMetadataKey = "GuFPAULudgD49S4+VDFck3LiqQ8sx4zmbrBtdpCSGcT+T0W0z4F5gYQYPlzTG6WOkdW5LJZK/"; String encryptedMetadataKey = "GuFPAULudgD49S4+VDFck3LiqQ8sx4zmbrBtdpCSGcT+T0W0z4F5gYQYPlzTG6WOkdW5LJZK/";
metadata.getMetadata().setMetadataKey(encryptedMetadataKey); metadata.getMetadata().setMetadataKey(encryptedMetadataKey);
@ -787,9 +785,8 @@ public class EncryptionTestIT extends AbstractIT {
// Helper // Helper
public static boolean compareJsonStrings(String expected, String actual) { public static boolean compareJsonStrings(String expected, String actual) {
JsonParser parser = new JsonParser(); JsonElement o1 = JsonParser.parseString(expected);
JsonElement o1 = parser.parse(expected); JsonElement o2 = JsonParser.parseString(actual);
JsonElement o2 = parser.parse(actual);
if (o1.equals(o2)) { if (o1.equals(o2)) {
return true; return true;
@ -828,7 +825,7 @@ public class EncryptionTestIT extends AbstractIT {
file1.setMetadataKey(0); file1.setMetadataKey(0);
file1.setAuthenticationTag("PboI9tqHHX3QeAA22PIu4w=="); file1.setAuthenticationTag("PboI9tqHHX3QeAA22PIu4w==");
files.put("ia7OEEEyXMoRa1QWQk8r", file1); files.put(filename, file1);
Data data2 = new Data(); Data data2 = new Data();
data2.setKey("9dfzbIYDt28zTyZfbcll+g=="); data2.setKey("9dfzbIYDt28zTyZfbcll+g==");
@ -841,70 +838,56 @@ public class EncryptionTestIT extends AbstractIT {
file2.setMetadataKey(0); file2.setMetadataKey(0);
file2.setAuthenticationTag("qOQZdu5soFO77Y7y4rAOVA=="); file2.setAuthenticationTag("qOQZdu5soFO77Y7y4rAOVA==");
files.put("n9WXAIXO2wRY4R8nXwmo", file2); files.put(secondFilename, file2);
return new DecryptedFolderMetadataFileV1(metadata1, files); return new DecryptedFolderMetadataFileV1(metadata1, files);
} }
private boolean cryptFile(String fileName, String md5, byte[] key, byte[] iv)
private boolean cryptFile(String fileName, String md5, byte[] key, byte[] iv, byte[] expectedAuthTag)
throws Exception { throws Exception {
File file = getFile(fileName); File file = File.createTempFile(fileName, "enc");
assertEquals(md5, getMD5Sum(file)); String md5BeforeEncryption = getMD5Sum(file);
EncryptedFile encryptedFile = encryptFile(file, key, iv); // Encryption
Cipher encryptorCipher = EncryptionUtils.getCipher(Cipher.ENCRYPT_MODE, key, iv);
File encryptedTempFile = File.createTempFile("file", "tmp"); EncryptionUtils.encryptFile(file, encryptorCipher);
FileOutputStream fileOutputStream = new FileOutputStream(encryptedTempFile); String encryptorCipherAuthTag = EncryptionUtils.getAuthenticationTag(encryptorCipher);
fileOutputStream.write(encryptedFile.getEncryptedBytes());
fileOutputStream.close();
byte[] authenticationTag = decodeStringToBase64Bytes(encryptedFile.getAuthenticationTag());
// verify authentication tag
assertTrue(Arrays.equals(expectedAuthTag, authenticationTag));
byte[] decryptedBytes = decryptFile(encryptedTempFile,
key,
iv,
authenticationTag,
new ArbitraryDataProviderImpl(targetContext),
user);
// Decryption
Cipher decryptorCipher = EncryptionUtils.getCipher(Cipher.DECRYPT_MODE, key, iv);
File decryptedFile = File.createTempFile("file", "dec"); File decryptedFile = File.createTempFile("file", "dec");
FileOutputStream fileOutputStream1 = new FileOutputStream(decryptedFile); decryptFile(decryptorCipher, file, decryptedFile, encryptorCipherAuthTag, new ArbitraryDataProviderImpl(targetContext), user);
fileOutputStream1.write(decryptedBytes);
fileOutputStream1.close();
return md5.compareTo(getMD5Sum(decryptedFile)) == 0; String md5AfterEncryption = getMD5Sum(decryptedFile);
if (md5BeforeEncryption == null) {
Assert.fail();
} }
private String getMD5Sum(File file) { return md5BeforeEncryption.equals(md5AfterEncryption);
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(file);
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] bytes = new byte[2048];
int readBytes;
while ((readBytes = fileInputStream.read(bytes)) != -1) {
md5.update(bytes, 0, readBytes);
} }
return new String(Hex.encodeHex(md5.digest())); public static String getMD5Sum(File file) {
try (FileInputStream fis = new FileInputStream(file)) {
} catch (Exception e) { MessageDigest md = MessageDigest.getInstance(MD5_ALGORITHM);
Log_OC.e(this, e.getMessage()); DigestInputStream dis = new DigestInputStream(fis, md);
} finally { byte[] buffer = new byte[4096];
if (fileInputStream != null) { int bytesRead;
try { while ((bytesRead = dis.read(buffer)) != -1) {
fileInputStream.close(); md.update(buffer, 0, bytesRead);
} catch (IOException e) {
Log_OC.e(this, "Error getting MD5 checksum for file", e);
} }
byte[] digest = md.digest();
return bytesToHex(digest);
} catch (IOException | NoSuchAlgorithmException e) {
return null;
} }
} }
return ""; private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} }
} }

View file

@ -133,7 +133,12 @@ interface BackgroundJobManager {
fun startImmediateFilesExportJob(files: Collection<OCFile>): LiveData<JobInfo?> fun startImmediateFilesExportJob(files: Collection<OCFile>): LiveData<JobInfo?>
fun schedulePeriodicFilesSyncJob() fun schedulePeriodicFilesSyncJob()
fun startImmediateFilesSyncJob(skipCustomFolders: Boolean = false, overridePowerSaving: Boolean = false)
fun startImmediateFilesSyncJob(
overridePowerSaving: Boolean = false,
changedFiles: Array<String> = arrayOf<String>()
)
fun scheduleOfflineSync() fun scheduleOfflineSync()
fun scheduleMediaFoldersDetectionJob() fun scheduleMediaFoldersDetectionJob()

View file

@ -98,7 +98,7 @@ internal class BackgroundJobManagerImpl(
const val JOB_TEST = "test_job" const val JOB_TEST = "test_job"
const val MAX_CONTENT_TRIGGER_DELAY_MS = 1500L const val MAX_CONTENT_TRIGGER_DELAY_MS = 10000L
const val TAG_PREFIX_NAME = "name" const val TAG_PREFIX_NAME = "name"
const val TAG_PREFIX_USER = "user" const val TAG_PREFIX_USER = "user"
@ -277,7 +277,7 @@ internal class BackgroundJobManagerImpl(
.setConstraints(constrains) .setConstraints(constrains)
.build() .build()
workManager.enqueueUniqueWork(JOB_CONTENT_OBSERVER, ExistingWorkPolicy.REPLACE, request) workManager.enqueueUniqueWork(JOB_CONTENT_OBSERVER, ExistingWorkPolicy.APPEND, request)
} }
override fun schedulePeriodicContactsBackup(user: User) { override fun schedulePeriodicContactsBackup(user: User) {
@ -425,10 +425,13 @@ internal class BackgroundJobManagerImpl(
workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_FILES_SYNC, ExistingPeriodicWorkPolicy.REPLACE, request) workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_FILES_SYNC, ExistingPeriodicWorkPolicy.REPLACE, request)
} }
override fun startImmediateFilesSyncJob(skipCustomFolders: Boolean, overridePowerSaving: Boolean) { override fun startImmediateFilesSyncJob(
overridePowerSaving: Boolean,
changedFiles: Array<String>
) {
val arguments = Data.Builder() val arguments = Data.Builder()
.putBoolean(FilesSyncWork.SKIP_CUSTOM, skipCustomFolders)
.putBoolean(FilesSyncWork.OVERRIDE_POWER_SAVING, overridePowerSaving) .putBoolean(FilesSyncWork.OVERRIDE_POWER_SAVING, overridePowerSaving)
.putStringArray(FilesSyncWork.CHANGED_FILES, changedFiles)
.build() .build()
val request = oneTimeRequestBuilder( val request = oneTimeRequestBuilder(
@ -438,7 +441,7 @@ internal class BackgroundJobManagerImpl(
.setInputData(arguments) .setInputData(arguments)
.build() .build()
workManager.enqueueUniqueWork(JOB_IMMEDIATE_FILES_SYNC, ExistingWorkPolicy.KEEP, request) workManager.enqueueUniqueWork(JOB_IMMEDIATE_FILES_SYNC, ExistingWorkPolicy.APPEND, request)
} }
override fun scheduleOfflineSync() { override fun scheduleOfflineSync() {

View file

@ -61,7 +61,11 @@ class ContentObserverWork(
private fun checkAndStartFileSyncJob() { private fun checkAndStartFileSyncJob() {
val syncFolders = syncerFolderProvider.countEnabledSyncedFolders() > 0 val syncFolders = syncerFolderProvider.countEnabledSyncedFolders() > 0
if (!powerManagementService.isPowerSavingEnabled && syncFolders) { if (!powerManagementService.isPowerSavingEnabled && syncFolders) {
backgroundJobManager.startImmediateFilesSyncJob(true, false) val changedFiles = mutableListOf<String>()
for (uri in params.triggeredContentUris) {
changedFiles.add(uri.toString())
}
backgroundJobManager.startImmediateFilesSyncJob(false, changedFiles.toTypedArray())
} }
} }

View file

@ -29,8 +29,8 @@ import android.os.Build
import android.text.TextUtils import android.text.TextUtils
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.device.PowerManagementService
@ -72,19 +72,23 @@ class FilesSyncWork(
private val powerManagementService: PowerManagementService, private val powerManagementService: PowerManagementService,
private val syncedFolderProvider: SyncedFolderProvider, private val syncedFolderProvider: SyncedFolderProvider,
private val backgroundJobManager: BackgroundJobManager private val backgroundJobManager: BackgroundJobManager
) : CoroutineWorker(context, params) { ) : Worker(context, params) {
companion object { companion object {
const val TAG = "FilesSyncJob" const val TAG = "FilesSyncJob"
const val SKIP_CUSTOM = "skipCustom" const val SKIP_CUSTOM = "skipCustom"
const val OVERRIDE_POWER_SAVING = "overridePowerSaving" const val OVERRIDE_POWER_SAVING = "overridePowerSaving"
const val CHANGED_FILES = "changedFiles"
const val FOREGROUND_SERVICE_ID = 414 const val FOREGROUND_SERVICE_ID = 414
} }
@Suppress("MagicNumber") @Suppress("MagicNumber")
private fun createForegroundInfo(progressPercent: Int): ForegroundInfo { private fun updateForegroundWorker(progressPercent: Int, useForegroundWorker: Boolean) {
// update throughout worker execution to give use feedback how far worker is if (useForegroundWorker) {
return
}
// update throughout worker execution to give use feedback how far worker is
val notification = NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_FILE_SYNC) val notification = NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_FILE_SYNC)
.setTicker(context.getString(R.string.autoupload_worker_foreground_info)) .setTicker(context.getString(R.string.autoupload_worker_foreground_info))
.setContentText(context.getString(R.string.autoupload_worker_foreground_info)) .setContentText(context.getString(R.string.autoupload_worker_foreground_info))
@ -93,17 +97,18 @@ class FilesSyncWork(
.setOngoing(true) .setOngoing(true)
.setProgress(100, progressPercent, false) .setProgress(100, progressPercent, false)
.build() .build()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val foregroundInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ForegroundInfo(FOREGROUND_SERVICE_ID, notification, ForegroundServiceType.DataSync.getId()) ForegroundInfo(FOREGROUND_SERVICE_ID, notification, ForegroundServiceType.DataSync.getId())
} else { } else {
ForegroundInfo(FOREGROUND_SERVICE_ID, notification) ForegroundInfo(FOREGROUND_SERVICE_ID, notification)
} }
setForegroundAsync(foregroundInfo)
} }
@Suppress("MagicNumber") @Suppress("MagicNumber")
override suspend fun doWork(): Result { override fun doWork(): Result {
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class)) backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
setForeground(createForegroundInfo(0))
val overridePowerSaving = inputData.getBoolean(OVERRIDE_POWER_SAVING, false) val overridePowerSaving = inputData.getBoolean(OVERRIDE_POWER_SAVING, false)
// If we are in power save mode, better to postpone upload // If we are in power save mode, better to postpone upload
@ -114,26 +119,35 @@ class FilesSyncWork(
} }
val resources = context.resources val resources = context.resources
val lightVersion = resources.getBoolean(R.bool.syncedFolder_light) val lightVersion = resources.getBoolean(R.bool.syncedFolder_light)
val skipCustom = inputData.getBoolean(SKIP_CUSTOM, false)
FilesSyncHelper.restartJobsIfNeeded( FilesSyncHelper.restartJobsIfNeeded(
uploadsStorageManager, uploadsStorageManager,
userAccountManager, userAccountManager,
connectivityService, connectivityService,
powerManagementService powerManagementService
) )
setForeground(createForegroundInfo(5))
FilesSyncHelper.insertAllDBEntries(skipCustom, syncedFolderProvider) // Get changed files from ContentObserverWork (only images and videos) or by scanning filesystem
setForeground(createForegroundInfo(50)) val changedFiles = inputData.getStringArray(CHANGED_FILES)
collectChangedFiles(changedFiles)
// Create all the providers we'll need // Create all the providers we'll need
val filesystemDataProvider = FilesystemDataProvider(contentResolver) val filesystemDataProvider = FilesystemDataProvider(contentResolver)
val currentLocale = resources.configuration.locale val currentLocale = resources.configuration.locale
val dateFormat = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale) val dateFormat = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale)
dateFormat.timeZone = TimeZone.getTimeZone(TimeZone.getDefault().id) dateFormat.timeZone = TimeZone.getTimeZone(TimeZone.getDefault().id)
// start upload of changed / new files
val syncedFolders = syncedFolderProvider.syncedFolders val syncedFolders = syncedFolderProvider.syncedFolders
for ((index, syncedFolder) in syncedFolders.withIndex()) { for ((index, syncedFolder) in syncedFolders.withIndex()) {
setForeground(createForegroundInfo((50 + (index.toDouble() / syncedFolders.size.toDouble()) * 50).toInt())) updateForegroundWorker(
if (syncedFolder.isEnabled && (!skipCustom || MediaFolderType.CUSTOM != syncedFolder.type)) { (50 + (index.toDouble() / syncedFolders.size.toDouble()) * 50).toInt(),
changedFiles.isNullOrEmpty()
)
if (syncedFolder.isEnabled && (
changedFiles.isNullOrEmpty() ||
MediaFolderType.CUSTOM != syncedFolder.type
)
) {
syncFolder( syncFolder(
context, context,
resources, resources,
@ -150,6 +164,19 @@ class FilesSyncWork(
return result return result
} }
@Suppress("MagicNumber")
private fun collectChangedFiles(changedFiles: Array<String>?) {
if (!changedFiles.isNullOrEmpty()) {
FilesSyncHelper.insertChangedEntries(syncedFolderProvider, changedFiles)
} else {
// Check every file in every synced folder for changes and update
// filesystemDataProvider database (potentially needs a long time so use foreground worker)
updateForegroundWorker(5, true)
FilesSyncHelper.insertAllDBEntries(syncedFolderProvider)
updateForegroundWorker(50, true)
}
}
@Suppress("LongMethod") // legacy code @Suppress("LongMethod") // legacy code
private fun syncFolder( private fun syncFolder(
context: Context, context: Context,

View file

@ -556,7 +556,7 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
} }
if (!preferences.isAutoUploadInitialized()) { if (!preferences.isAutoUploadInitialized()) {
backgroundJobManager.startImmediateFilesSyncJob(false, false); backgroundJobManager.startImmediateFilesSyncJob(false, new String[]{});
preferences.setAutoUploadInit(true); preferences.setAutoUploadInit(true);
} }

View file

@ -278,4 +278,8 @@ public class SyncedFolder implements Serializable, Cloneable {
public void setExcludeHidden(boolean excludeHidden) { public void setExcludeHidden(boolean excludeHidden) {
this.excludeHidden = excludeHidden; this.excludeHidden = excludeHidden;
} }
public boolean containsFile(String filePath){
return filePath.contains(localPath);
}
} }

View file

@ -21,4 +21,6 @@
*/ */
package com.owncloud.android.datamodel.e2e.v1.encrypted package com.owncloud.android.datamodel.e2e.v1.encrypted
class EncryptedFile(var encryptedBytes: ByteArray, var authenticationTag: String) import java.io.File
class EncryptedFile(var encryptedFile: File, var authenticationTag: String)

View file

@ -51,6 +51,8 @@ import java.util.Iterator;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import javax.crypto.Cipher;
import static com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes; import static com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes;
/** /**
@ -262,31 +264,16 @@ public class DownloadFileOperation extends RemoteOperation {
byte[] key = decodeStringToBase64Bytes(keyString); byte[] key = decodeStringToBase64Bytes(keyString);
byte[] iv = decodeStringToBase64Bytes(nonceString); byte[] iv = decodeStringToBase64Bytes(nonceString);
byte[] authenticationTag = decodeStringToBase64Bytes(authenticationTagString);
try { try {
byte[] decryptedBytes = EncryptionUtils.decryptFile(tmpFile, Cipher cipher = EncryptionUtils.getCipher(Cipher.DECRYPT_MODE, key, iv);
key, EncryptionUtils.decryptFile(cipher, tmpFile, newFile, authenticationTagString, new ArbitraryDataProviderImpl(operationContext), user);
iv,
authenticationTag,
new ArbitraryDataProviderImpl(operationContext),
user);
try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile)) {
fileOutputStream.write(decryptedBytes);
}
} catch (Exception e) { } catch (Exception e) {
return new RemoteOperationResult(e); return new RemoteOperationResult(e);
} }
} }
if (downloadType == DownloadType.DOWNLOAD) { if (downloadType == DownloadType.EXPORT) {
moved = tmpFile.renameTo(newFile);
newFile.setLastModified(file.getModificationTimestamp());
if (!moved) {
result = new RemoteOperationResult(RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_MOVED);
}
} else if (downloadType == DownloadType.EXPORT) {
new FileExportUtils().exportFile(file.getFileName(), new FileExportUtils().exportFile(file.getFileName(),
file.getMimeType(), file.getMimeType(),
operationContext.getContentResolver(), operationContext.getContentResolver(),

View file

@ -94,6 +94,8 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import javax.crypto.Cipher;
import androidx.annotation.CheckResult; import androidx.annotation.CheckResult;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -558,14 +560,11 @@ public class UploadFileOperation extends SyncOperation {
Long creationTimestamp = FileUtil.getCreationTimestamp(originalFile); Long creationTimestamp = FileUtil.getCreationTimestamp(originalFile);
/***** E2E *****/ /***** E2E *****/
// Key, always generate new one
byte[] key = EncryptionUtils.generateKey(); byte[] key = EncryptionUtils.generateKey();
// IV, always generate new one
byte[] iv = EncryptionUtils.randomBytes(EncryptionUtils.ivLength); byte[] iv = EncryptionUtils.randomBytes(EncryptionUtils.ivLength);
Cipher cipher = EncryptionUtils.getCipher(Cipher.ENCRYPT_MODE, key, iv);
EncryptedFile encryptedFile = EncryptionUtils.encryptFile(mFile, key, iv); File file = new File(mFile.getStoragePath());
EncryptedFile encryptedFile = EncryptionUtils.encryptFile(file, cipher);
// new random file name, check if it exists in metadata // new random file name, check if it exists in metadata
String encryptedFileName = EncryptionUtils.generateUid(); String encryptedFileName = EncryptionUtils.generateUid();
@ -580,10 +579,7 @@ public class UploadFileOperation extends SyncOperation {
} }
} }
File encryptedTempFile = File.createTempFile("encFile", encryptedFileName); File encryptedTempFile = encryptedFile.getEncryptedFile();
FileOutputStream fileOutputStream = new FileOutputStream(encryptedTempFile);
fileOutputStream.write(encryptedFile.getEncryptedBytes());
fileOutputStream.close();
/***** E2E *****/ /***** E2E *****/
@ -742,6 +738,8 @@ public class UploadFileOperation extends SyncOperation {
token = null; token = null;
} }
} }
encryptedTempFile.delete();
} }
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Log_OC.d(TAG, mFile.getStoragePath() + " not exists anymore"); Log_OC.d(TAG, mFile.getStoragePath() + " not exists anymore");

View file

@ -40,7 +40,6 @@ import androidx.recyclerview.widget.GridLayoutManager
import com.nextcloud.client.core.Clock import com.nextcloud.client.core.Clock
import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.device.PowerManagementService
import com.nextcloud.client.di.Injectable import com.nextcloud.client.di.Injectable
import com.nextcloud.client.jobs.BackgroundJobManager
import com.nextcloud.client.jobs.MediaFoldersDetectionWork import com.nextcloud.client.jobs.MediaFoldersDetectionWork
import com.nextcloud.client.jobs.NotificationWork import com.nextcloud.client.jobs.NotificationWork
import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.jobs.upload.FileUploadWorker
@ -156,9 +155,6 @@ class SyncedFoldersActivity :
@Inject @Inject
lateinit var clock: Clock lateinit var clock: Clock
@Inject
lateinit var backgroundJobManager: BackgroundJobManager
@Inject @Inject
lateinit var viewThemeUtils: ViewThemeUtils lateinit var viewThemeUtils: ViewThemeUtils
@ -584,7 +580,7 @@ class SyncedFoldersActivity :
} }
} }
if (syncedFolderDisplayItem.isEnabled) { if (syncedFolderDisplayItem.isEnabled) {
backgroundJobManager.startImmediateFilesSyncJob(skipCustomFolders = false, overridePowerSaving = false) backgroundJobManager.startImmediateFilesSyncJob(overridePowerSaving = false)
showBatteryOptimizationInfo() showBatteryOptimizationInfo()
} }
} }
@ -714,7 +710,7 @@ class SyncedFoldersActivity :
// existing synced folder setup to be updated // existing synced folder setup to be updated
syncedFolderProvider.updateSyncFolder(item) syncedFolderProvider.updateSyncFolder(item)
if (item.isEnabled) { if (item.isEnabled) {
backgroundJobManager.startImmediateFilesSyncJob(skipCustomFolders = false, overridePowerSaving = false) backgroundJobManager.startImmediateFilesSyncJob(overridePowerSaving = false)
} else { } else {
val syncedFolderInitiatedKey = KEY_SYNCED_FOLDER_INITIATED_PREFIX + item.id val syncedFolderInitiatedKey = KEY_SYNCED_FOLDER_INITIATED_PREFIX + item.id
val arbitraryDataProvider = val arbitraryDataProvider =
@ -731,7 +727,7 @@ class SyncedFoldersActivity :
if (storedId != -1L) { if (storedId != -1L) {
item.id = storedId item.id = storedId
if (item.isEnabled) { if (item.isEnabled) {
backgroundJobManager.startImmediateFilesSyncJob(skipCustomFolders = false, overridePowerSaving = false) backgroundJobManager.startImmediateFilesSyncJob(overridePowerSaving = false)
} else { } else {
val syncedFolderInitiatedKey = KEY_SYNCED_FOLDER_INITIATED_PREFIX + item.id val syncedFolderInitiatedKey = KEY_SYNCED_FOLDER_INITIATED_PREFIX + item.id
arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey) arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey)

View file

@ -208,7 +208,7 @@ public class UploadListActivity extends FileActivity {
} }
private void refresh() { private void refresh() {
backgroundJobManager.startImmediateFilesSyncJob(false, true); backgroundJobManager.startImmediateFilesSyncJob(true,new String[]{});
if (uploadsStorageManager.getFailedUploads().length > 0) { if (uploadsStorageManager.getFailedUploads().length > 0) {
new Thread(() -> { new Thread(() -> {

View file

@ -22,11 +22,13 @@
package com.owncloud.android.utils; package com.owncloud.android.utils;
import android.content.Context; import android.content.Context;
import android.os.Build;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Base64; import android.util.Base64;
import android.util.Pair; import android.util.Pair;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.primitives.Bytes;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
@ -71,13 +73,17 @@ import org.apache.commons.httpclient.HttpStatus;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.Key; import java.security.Key;
@ -95,6 +101,7 @@ import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateCrtKey; import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPublicKey; import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec; import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList; import java.util.ArrayList;
@ -107,6 +114,8 @@ import java.util.UUID;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException; import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator; import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
@ -118,6 +127,7 @@ import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@ -554,95 +564,80 @@ public final class EncryptionUtils {
return Base64.decode(string, Base64.NO_WRAP); return Base64.decode(string, Base64.NO_WRAP);
} }
/* public static EncryptedFile encryptFile(File file, Cipher cipher) throws InvalidParameterSpecException {
ENCRYPTION File encryptedFile = new File(file.getAbsolutePath() + ".enc");
*/ encryptFileWithGivenCipher(file, encryptedFile, cipher);
String authenticationTagString = getAuthenticationTag(cipher);
/** return new EncryptedFile(encryptedFile, authenticationTagString);
* @param ocFile file do crypt
* @param encryptionKeyBytes key, either from metadata or {@link EncryptionUtils#generateKey()}
* @param iv initialization vector, either from metadata or
* {@link EncryptionUtils#randomBytes(int)}
* @return encryptedFile with encryptedBytes and authenticationTag
*/
public static EncryptedFile encryptFile(OCFile ocFile, byte[] encryptionKeyBytes, byte[] iv)
throws NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException, IOException {
File file = new File(ocFile.getStoragePath());
return encryptFile(file, encryptionKeyBytes, iv);
} }
/** public static String getAuthenticationTag(Cipher cipher) throws InvalidParameterSpecException {
* @param file file do crypt byte[] authenticationTag = cipher.getParameters().getParameterSpec(GCMParameterSpec.class).getIV();
* @param encryptionKeyBytes key, either from metadata or {@link EncryptionUtils#generateKey()} return encodeBytesToBase64String(authenticationTag);
* @param iv initialization vector, either from metadata or }
* {@link EncryptionUtils#randomBytes(int)}
* @return encryptedFile with encryptedBytes and authenticationTag
*/
public static EncryptedFile encryptFile(File file, byte[] encryptionKeyBytes, byte[] iv)
throws NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException, IOException {
public static Cipher getCipher(int mode, byte[] encryptionKeyBytes, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
Cipher cipher = Cipher.getInstance(AES_CIPHER); Cipher cipher = Cipher.getInstance(AES_CIPHER);
Key key = new SecretKeySpec(encryptionKeyBytes, AES); Key key = new SecretKeySpec(encryptionKeyBytes, AES);
GCMParameterSpec spec = new GCMParameterSpec(128, iv); GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec); cipher.init(mode, key, spec);
return cipher;
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
byte[] fileBytes = new byte[(int) randomAccessFile.length()];
randomAccessFile.readFully(fileBytes);
byte[] cryptedBytes = cipher.doFinal(fileBytes);
String authenticationTag = encodeBytesToBase64String(Arrays.copyOfRange(cryptedBytes,
cryptedBytes.length - (128 / 8),
cryptedBytes.length));
return new EncryptedFile(cryptedBytes, authenticationTag);
} }
/** public static void encryptFileWithGivenCipher(File inputFile, File encryptedFile, Cipher cipher) {
* @param file encrypted file try( FileInputStream inputStream = new FileInputStream(inputFile);
* @param encryptionKeyBytes key from metadata FileOutputStream fileOutputStream = new FileOutputStream(encryptedFile);
* @param iv initialization vector from metadata CipherOutputStream outputStream = new CipherOutputStream(fileOutputStream, cipher)) {
* @param authenticationTag authenticationTag from metadata byte[] buffer = new byte[4096];
* @return decrypted byte[] int bytesRead;
*/
public static byte[] decryptFile(File file, while ((bytesRead = inputStream.read(buffer)) != -1) {
byte[] encryptionKeyBytes, outputStream.write(buffer, 0, bytesRead);
byte[] iv, }
byte[] authenticationTag,
outputStream.close();
inputStream.close();
Log_OC.d(TAG, encryptedFile.getName() + "encrypted successfully");
} catch (IOException exception) {
Log_OC.d(TAG, "Error caught at encryptFileWithGivenCipher(): " + exception.getLocalizedMessage());
}
}
public static void decryptFile(Cipher cipher,
File encryptedFile,
File decryptedFile,
String authenticationTag,
ArbitraryDataProvider arbitraryDataProvider, ArbitraryDataProvider arbitraryDataProvider,
User user) User user) {
throws NoSuchAlgorithmException, try (FileInputStream inputStream = new FileInputStream(encryptedFile);
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException, FileOutputStream outputStream = new FileOutputStream(decryptedFile)) {
BadPaddingException, IllegalBlockSizeException, IOException {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
byte[] output = cipher.update(buffer, 0, bytesRead);
if (output != null) {
outputStream.write(output);
}
}
byte[] output = cipher.doFinal();
if (output != null) {
outputStream.write(output);
}
inputStream.close();
outputStream.close();
Cipher cipher = Cipher.getInstance(AES_CIPHER); if (!getAuthenticationTag(cipher).equals(authenticationTag)) {
Key key = new SecretKeySpec(encryptionKeyBytes, AES);
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
byte[] fileBytes = new byte[(int) randomAccessFile.length()];
randomAccessFile.readFully(fileBytes);
// check authentication tag
byte[] extractedAuthenticationTag = Arrays.copyOfRange(fileBytes,
fileBytes.length - (128 / 8),
fileBytes.length);
if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) {
reportE2eError(arbitraryDataProvider, user); reportE2eError(arbitraryDataProvider, user);
throw new SecurityException("Tag not correct"); throw new SecurityException("Tag not correct");
} }
return cipher.doFinal(fileBytes); Log_OC.d(TAG, encryptedFile.getName() + "decrypted successfully");
} catch (IOException | BadPaddingException | IllegalBlockSizeException | InvalidParameterSpecException |
SecurityException exception) {
Log_OC.d(TAG, "Error caught at decryptFile(): " + exception.getLocalizedMessage());
}
} }
/** /**

View file

@ -130,15 +130,51 @@ public final class FilesSyncHelper {
} }
} }
public static void insertAllDBEntries(boolean skipCustom, public static void insertAllDBEntries(SyncedFolderProvider syncedFolderProvider) {
SyncedFolderProvider syncedFolderProvider) {
for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) { for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
if (syncedFolder.isEnabled() && (!skipCustom || syncedFolder.getType() != MediaFolderType.CUSTOM)) { if (syncedFolder.isEnabled()) {
insertAllDBEntriesForSyncedFolder(syncedFolder); insertAllDBEntriesForSyncedFolder(syncedFolder);
} }
} }
} }
public static void insertChangedEntries(SyncedFolderProvider syncedFolderProvider,
String[] changedFiles) {
final ContentResolver contentResolver = MainApp.getAppContext().getContentResolver();
final FilesystemDataProvider filesystemDataProvider = new FilesystemDataProvider(contentResolver);
for (String changedFileURI : changedFiles){
String changedFile = getFileFromURI(changedFileURI);
for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
if (syncedFolder.isEnabled() && syncedFolder.containsFile(changedFile)){
File file = new File(changedFile);
filesystemDataProvider.storeOrUpdateFileValue(changedFile,
file.lastModified(),file.isDirectory(),
syncedFolder);
break;
}
}
}
}
private static String getFileFromURI(String uri){
final Context context = MainApp.getAppContext();
Cursor cursor;
int column_index_data;
String filePath = null;
String[] projection = {MediaStore.MediaColumns.DATA};
cursor = context.getContentResolver().query(Uri.parse(uri), projection, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
column_index_data = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA);
filePath = cursor.getString(column_index_data);
cursor.close();
}
return filePath;
}
private static void insertContentIntoDB(Uri uri, SyncedFolder syncedFolder) { private static void insertContentIntoDB(Uri uri, SyncedFolder syncedFolder) {
final Context context = MainApp.getAppContext(); final Context context = MainApp.getAppContext();
final ContentResolver contentResolver = context.getContentResolver(); final ContentResolver contentResolver = context.getContentResolver();

View file

@ -766,6 +766,7 @@
<string name="shared_icon_shared_via_link">compartido mediante enlace</string> <string name="shared_icon_shared_via_link">compartido mediante enlace</string>
<string name="shared_with_you_by">Compartido con Ud. por %1$s</string> <string name="shared_with_you_by">Compartido con Ud. por %1$s</string>
<string name="sharee_add_failed">Se presentó una falla al agregar una persona para compartir</string> <string name="sharee_add_failed">Se presentó una falla al agregar una persona para compartir</string>
<string name="sharee_already_added_to_file">No se pudo añadir el recurso compartido. Este archivo o carpeta ya se ha compartido con esta persona o grupo.</string>
<string name="show_images">Mostrar fotos</string> <string name="show_images">Mostrar fotos</string>
<string name="show_video">Mostrar videos</string> <string name="show_video">Mostrar videos</string>
<string name="signup_with_provider">Registrarse con el proveedor</string> <string name="signup_with_provider">Registrarse con el proveedor</string>

View file

@ -740,7 +740,7 @@
<string name="share_remote_clarification">%1$s (distant)</string> <string name="share_remote_clarification">%1$s (distant)</string>
<string name="share_room_clarification">%1$s (conversation)</string> <string name="share_room_clarification">%1$s (conversation)</string>
<string name="share_search">Nom, ID de Cloud Fédéré ou adresse e-mail…</string> <string name="share_search">Nom, ID de Cloud Fédéré ou adresse e-mail…</string>
<string name="share_send_new_email">Envoyer un nouvel e-mail</string> <string name="share_send_new_email">Envoyer un nouveau courriel</string>
<string name="share_send_note">Note au destinataire</string> <string name="share_send_note">Note au destinataire</string>
<string name="share_settings">Paramètres</string> <string name="share_settings">Paramètres</string>
<string name="share_via_link_hide_download">Masquer le téléchargement</string> <string name="share_via_link_hide_download">Masquer le téléchargement</string>
@ -981,7 +981,7 @@
<string name="widgets_not_available">Les widgets ne sont disponibles que sur %1$s 25 ou plus tard</string> <string name="widgets_not_available">Les widgets ne sont disponibles que sur %1$s 25 ou plus tard</string>
<string name="widgets_not_available_title">Non disponible</string> <string name="widgets_not_available_title">Non disponible</string>
<string name="worker_download">Réception de fichiers…</string> <string name="worker_download">Réception de fichiers…</string>
<string name="write_email">Envoyer un e-mail</string> <string name="write_email">Envoyer un courriel</string>
<string name="wrong_storage_path">Le dossier de stockage des données n\'existe pas !</string> <string name="wrong_storage_path">Le dossier de stockage des données n\'existe pas !</string>
<string name="wrong_storage_path_desc">Cela peut être dû à une restauration de sauvegarde sur un autre appareil. Retour aux valeurs par défaut. Veuillez vérifier les paramètres pour ajuster le chemin de stockage.</string> <string name="wrong_storage_path_desc">Cela peut être dû à une restauration de sauvegarde sur un autre appareil. Retour aux valeurs par défaut. Veuillez vérifier les paramètres pour ajuster le chemin de stockage.</string>
<plurals name="sync_fail_in_favourites_content"> <plurals name="sync_fail_in_favourites_content">

View file

@ -38,8 +38,22 @@
<string name="app_widget_description">Zobrazí jeden widget z hlavného panela</string> <string name="app_widget_description">Zobrazí jeden widget z hlavného panela</string>
<string name="appbar_search_in">Hľadať v %s</string> <string name="appbar_search_in">Hľadať v %s</string>
<string name="assistant_screen_all_task_type">Všetko</string> <string name="assistant_screen_all_task_type">Všetko</string>
<string name="assistant_screen_create_task_alert_dialog_input_field_placeholder">Napíšte nejaký text</string>
<string name="assistant_screen_delete_task_alert_dialog_description">Naozaj chcete vymazať túto úlohu?</string>
<string name="assistant_screen_delete_task_alert_dialog_title">Vymazať Úlohu</string>
<string name="assistant_screen_loading">Zoznam úloh sa nahráva, prosím čakajte</string>
<string name="assistant_screen_no_task_available_for_all_task_filter_text">Žiadne úlohy nie su dostupné. Vyberte typ úlohy pre vytvorenie novej.</string>
<string name="assistant_screen_no_task_available_text">Pre typ úlohy %s nie je k dispozícii žiadna úloha, môžete vytvoriť novú úlohu vpravo dole.</string>
<string name="assistant_screen_task_create_fail_message">Pri vytváraní úlohy nastala chyba</string>
<string name="assistant_screen_task_create_success_message">Úloha bola úspešne vytvorená</string>
<string name="assistant_screen_task_delete_fail_message">Úloha bola úspešne odstránená</string>
<string name="assistant_screen_task_delete_success_message">Pri odstraňovaní úlohy nastala chyba</string>
<string name="assistant_screen_task_list_error_state_message">Nepodarilo sa načítať zoznam úloh. Skontrolujte pripojenie na internet.</string>
<string name="assistant_screen_task_more_actions_bottom_sheet_delete_action">Vymazať Úlohu</string>
<string name="assistant_screen_task_types_error_state_message">Nepodarilo sa načítať zoznam typov úloh, prosím, skontrolujte pripojenie na internet.</string>
<string name="assistant_screen_task_view_show_less">Zobraziť menej</string> <string name="assistant_screen_task_view_show_less">Zobraziť menej</string>
<string name="assistant_screen_task_view_show_more">Zobraziť viac</string> <string name="assistant_screen_task_view_show_more">Zobraziť viac</string>
<string name="assistant_screen_top_bar_title">Asistent</string>
<string name="associated_account_not_found">Priradený účet sa nenašiel</string> <string name="associated_account_not_found">Priradený účet sa nenašiel</string>
<string name="auth_access_failed">Prístup zamietnutý: %1$s</string> <string name="auth_access_failed">Prístup zamietnutý: %1$s</string>
<string name="auth_account_does_not_exist">Účet zatiaľ v zariadení neexistuje</string> <string name="auth_account_does_not_exist">Účet zatiaľ v zariadení neexistuje</string>
@ -238,6 +252,7 @@
<string name="drawer_header_background">Obrázok na pozadí hlavičky panela</string> <string name="drawer_header_background">Obrázok na pozadí hlavičky panela</string>
<string name="drawer_item_activities">Aktivity</string> <string name="drawer_item_activities">Aktivity</string>
<string name="drawer_item_all_files">Všetky súbory</string> <string name="drawer_item_all_files">Všetky súbory</string>
<string name="drawer_item_assistant">Asistent</string>
<string name="drawer_item_favorites">Obľúbené</string> <string name="drawer_item_favorites">Obľúbené</string>
<string name="drawer_item_gallery">Média</string> <string name="drawer_item_gallery">Média</string>
<string name="drawer_item_groupfolders">Skupinové priečinky</string> <string name="drawer_item_groupfolders">Skupinové priečinky</string>
@ -256,6 +271,7 @@
<string name="drawer_synced_folders">Automatické nahratie</string> <string name="drawer_synced_folders">Automatické nahratie</string>
<string name="e2e_not_yet_setup">E2E zatiaľ nie je nastavené</string> <string name="e2e_not_yet_setup">E2E zatiaľ nie je nastavené</string>
<string name="e2e_offline">Nie je možné bez internetového pripojenia</string> <string name="e2e_offline">Nie je možné bez internetového pripojenia</string>
<string name="ecosystem_apps_display_assistant">Asistent</string>
<string name="ecosystem_apps_display_more">Viac</string> <string name="ecosystem_apps_display_more">Viac</string>
<string name="ecosystem_apps_display_notes">Poznámky</string> <string name="ecosystem_apps_display_notes">Poznámky</string>
<string name="ecosystem_apps_display_talk">Talk /Rozhovor/</string> <string name="ecosystem_apps_display_talk">Talk /Rozhovor/</string>
@ -750,6 +766,7 @@
<string name="shared_icon_shared_via_link">sprístupnené prostredníctvom odkazu</string> <string name="shared_icon_shared_via_link">sprístupnené prostredníctvom odkazu</string>
<string name="shared_with_you_by">Sprístupnené vám používateľom %1$s</string> <string name="shared_with_you_by">Sprístupnené vám používateľom %1$s</string>
<string name="sharee_add_failed">Pridanie sprístupňujúceho sa nepodarilo</string> <string name="sharee_add_failed">Pridanie sprístupňujúceho sa nepodarilo</string>
<string name="sharee_already_added_to_file">Pridanie zdieľania zlyhalo. Tento súbor alebo adresár už bol zdieľaný s touto osobou alebo skupinou.</string>
<string name="show_images">Zobraziť fotky</string> <string name="show_images">Zobraziť fotky</string>
<string name="show_video">Zobraziť videá</string> <string name="show_video">Zobraziť videá</string>
<string name="signup_with_provider">Zaregistrovať sa u poskytovateľa</string> <string name="signup_with_provider">Zaregistrovať sa u poskytovateľa</string>