mirror of
https://github.com/nextcloud/android.git
synced 2024-12-22 08:44:34 +03:00
Merge remote-tracking branch 'origin/master' into dev
This commit is contained in:
commit
caa54f762e
19 changed files with 297 additions and 239 deletions
|
@ -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"
|
||||||
|
|
|
@ -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())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(() -> {
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue