Skip to content

Commit

Permalink
Merge pull request #12272 from nextcloud/backport/12009/stable-3.27
Browse files Browse the repository at this point in the history
[stable-3.27] Report client health
  • Loading branch information
tobiasKaminsky committed Dec 11, 2023
2 parents d35d832 + eef3aab commit d2726e5
Show file tree
Hide file tree
Showing 25 changed files with 1,522 additions and 44 deletions.
1,179 changes: 1,179 additions & 0 deletions app/schemas/com.nextcloud.client.database.NextcloudDatabase/76.json

Large diffs are not rendered by default.

Expand Up @@ -75,4 +75,24 @@ class ArbitraryDataProviderIT : AbstractIT() {
arbitraryDataProvider.storeOrUpdateKeyValue(user.accountName, key, value.toString())
assertEquals(value, arbitraryDataProvider.getIntegerValue(user.accountName, key))
}

@Test
fun testIncrement() {
val key = "INCREMENT"

// key does not exist
assertEquals(-1, arbitraryDataProvider.getIntegerValue(user.accountName, key))

// increment -> 1
arbitraryDataProvider.incrementValue(user.accountName, key)
assertEquals(1, arbitraryDataProvider.getIntegerValue(user.accountName, key))

// increment -> 2
arbitraryDataProvider.incrementValue(user.accountName, key)
assertEquals(2, arbitraryDataProvider.getIntegerValue(user.accountName, key))

// delete
arbitraryDataProvider.deleteKeyForAccount(user.accountName, key)
assertEquals(-1, arbitraryDataProvider.getIntegerValue(user.accountName, key))
}
}
Expand Up @@ -765,7 +765,12 @@ private boolean cryptFile(String fileName, String md5, byte[] key, byte[] iv, by
// verify authentication tag
assertTrue(Arrays.equals(expectedAuthTag, authenticationTag));

byte[] decryptedBytes = decryptFile(encryptedTempFile, key, iv, authenticationTag);
byte[] decryptedBytes = decryptFile(encryptedTempFile,
key,
iv,
authenticationTag,
new ArbitraryDataProviderImpl(targetContext),
user);

File decryptedFile = File.createTempFile("file", "dec");
FileOutputStream fileOutputStream1 = new FileOutputStream(decryptedFile);
Expand Down
Expand Up @@ -68,7 +68,8 @@ import com.owncloud.android.db.ProviderMeta
AutoMigration(from = 71, to = 72),
AutoMigration(from = 72, to = 73),
AutoMigration(from = 73, to = 74, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 74, to = 75)
AutoMigration(from = 74, to = 75),
AutoMigration(from = 75, to = 76)
],
exportSchema = true
)
Expand Down
Expand Up @@ -61,4 +61,7 @@ interface FileDao {

@Query("SELECT * FROM filelist WHERE path LIKE :pathPattern AND file_owner = :fileOwner ORDER BY path ASC")
fun getFolderWithDescendants(pathPattern: String, fileOwner: String): List<FileEntity>

@Query("SELECT * FROM filelist where file_owner = :fileOwner AND etag_in_conflict IS NOT NULL")
fun getFilesWithSyncConflict(fileOwner: String): List<FileEntity>
}
Expand Up @@ -131,5 +131,7 @@ data class CapabilityEntity(
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_GROUPFOLDERS)
val groupfolders: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT)
val dropAccount: Int?
val dropAccount: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SECURITY_GUARD)
val securityGuard: Int?
)
4 changes: 2 additions & 2 deletions app/src/main/java/com/nextcloud/client/di/AppModule.java
Expand Up @@ -145,8 +145,8 @@ FilesRepository filesRepository(UserAccountManager accountManager, ClientFactory
}

@Provides
UploadsStorageManager uploadsStorageManager(Context context,
CurrentAccountProvider currentAccountProvider) {
UploadsStorageManager uploadsStorageManager(CurrentAccountProvider currentAccountProvider,
Context context) {
return new UploadsStorageManager(currentAccountProvider, context.getContentResolver());
}

Expand Down
Expand Up @@ -62,7 +62,7 @@ class BackgroundJobFactory @Inject constructor(
private val deviceInfo: DeviceInfo,
private val accountManager: UserAccountManager,
private val resources: Resources,
private val dataProvider: ArbitraryDataProvider,
private val arbitraryDataProvider: ArbitraryDataProvider,
private val uploadsStorageManager: UploadsStorageManager,
private val connectivityService: ConnectivityService,
private val notificationManager: NotificationManager,
Expand Down Expand Up @@ -103,6 +103,7 @@ class BackgroundJobFactory @Inject constructor(
FilesExportWork::class -> createFilesExportWork(context, workerParameters)
FilesUploadWorker::class -> createFilesUploadWorker(context, workerParameters)
GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters)
HealthStatusWork::class -> createHealthStatusWork(context, workerParameters)
else -> null // caller falls back to default factory
}
}
Expand Down Expand Up @@ -139,7 +140,7 @@ class BackgroundJobFactory @Inject constructor(
context,
params,
resources,
dataProvider,
arbitraryDataProvider,
contentResolver,
accountManager
)
Expand Down Expand Up @@ -260,4 +261,13 @@ class BackgroundJobFactory @Inject constructor(
params = params
)
}

private fun createHealthStatusWork(context: Context, params: WorkerParameters): HealthStatusWork {
return HealthStatusWork(
context,
params,
accountManager,
arbitraryDataProvider
)
}
}
Expand Up @@ -147,4 +147,6 @@ interface BackgroundJobManager {

fun pruneJobs()
fun cancelAllJobs()
fun schedulePeriodicHealthStatus()
fun startHealthStatus()
}
Expand Up @@ -82,6 +82,8 @@ internal class BackgroundJobManagerImpl(
const val JOB_PDF_GENERATION = "pdf_generation"
const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup"
const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export"
const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status"
const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status"

const val JOB_TEST = "test_job"

Expand Down Expand Up @@ -507,4 +509,25 @@ internal class BackgroundJobManagerImpl(
override fun cancelAllJobs() {
workManager.cancelAllWorkByTag(TAG_ALL)
}

override fun schedulePeriodicHealthStatus() {
val request = periodicRequestBuilder(
jobClass = HealthStatusWork::class,
jobName = JOB_PERIODIC_HEALTH_STATUS,
intervalMins = PERIODIC_BACKUP_INTERVAL_MINUTES
).build()

workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_HEALTH_STATUS, ExistingPeriodicWorkPolicy.KEEP, request)
}

override fun startHealthStatus() {
val request = oneTimeRequestBuilder(HealthStatusWork::class, JOB_IMMEDIATE_HEALTH_STATUS)
.build()

workManager.enqueueUniqueWork(
JOB_IMMEDIATE_HEALTH_STATUS,
ExistingWorkPolicy.KEEP,
request
)
}
}
131 changes: 131 additions & 0 deletions app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt
@@ -0,0 +1,131 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.nextcloud.client.jobs

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
import com.owncloud.android.datamodel.ArbitraryDataProvider
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.UploadsStorageManager
import com.owncloud.android.db.UploadResult
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.lib.resources.status.Problem
import com.owncloud.android.lib.resources.status.SendClientDiagnosticRemoteOperation
import com.owncloud.android.utils.EncryptionUtils
import com.owncloud.android.utils.theme.CapabilityUtils

class HealthStatusWork(
private val context: Context,
params: WorkerParameters,
private val userAccountManager: UserAccountManager,
private val arbitraryDataProvider: ArbitraryDataProvider
) : Worker(context, params) {
override fun doWork(): Result {
for (user in userAccountManager.allUsers) {
// only if security guard is enabled
if (!CapabilityUtils.getCapability(user, context).securityGuard.isTrue) {
continue
}

val syncConflicts = collectSyncConflicts(user)

val problems = mutableListOf<Problem>().apply {
addAll(
collectUploadProblems(
user,
listOf(
UploadResult.CREDENTIAL_ERROR,
UploadResult.CANNOT_CREATE_FILE,
UploadResult.FOLDER_ERROR,
UploadResult.SERVICE_INTERRUPTED
)
)
)
}

val virusDetected = collectUploadProblems(user, listOf(UploadResult.VIRUS_DETECTED)).firstOrNull()

val e2eErrors = EncryptionUtils.readE2eError(arbitraryDataProvider, user)

val nextcloudClient = OwnCloudClientManagerFactory.getDefaultSingleton()
.getNextcloudClientFor(user.toOwnCloudAccount(), context)
val result =
SendClientDiagnosticRemoteOperation(
syncConflicts,
problems,
virusDetected,
e2eErrors
).execute(
nextcloudClient
)

if (!result.isSuccess) {
if (result.exception == null) {
Log_OC.e(TAG, "Update client health NOT successful!")
} else {
Log_OC.e(TAG, "Update client health NOT successful!", result.exception)
}
}
}

return Result.success()
}

private fun collectSyncConflicts(user: User): Problem? {
val fileDataStorageManager = FileDataStorageManager(user, context.contentResolver)

val conflicts = fileDataStorageManager.getFilesWithSyncConflict(user)

return if (conflicts.isEmpty()) {
null
} else {
Problem("sync_conflicts", conflicts.size, conflicts.minOf { it.lastSyncDateForData })
}
}

private fun collectUploadProblems(user: User, errorCodes: List<UploadResult>): List<Problem> {
val uploadsStorageManager = UploadsStorageManager(userAccountManager, context.contentResolver)

val problems = uploadsStorageManager
.getUploadsForAccount(user.accountName)
.filter {
errorCodes.contains(it.lastResult)
}.groupBy { it.lastResult }

return if (problems.isEmpty()) {
emptyList()
} else {
return problems.map { problem ->
Problem(problem.key.toString(), problem.value.size, problem.value.minOf { it.uploadEndTimestamp })
}
}
}

companion object {
private const val TAG = "Health Status"
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/com/owncloud/android/MainApp.java
Expand Up @@ -349,6 +349,8 @@ public void onCreate() {
backgroundJobManager.scheduleMediaFoldersDetectionJob();
backgroundJobManager.startMediaFoldersDetectionJob();

backgroundJobManager.schedulePeriodicHealthStatus();

registerGlobalPassCodeProtection();
}

Expand Down
Expand Up @@ -28,6 +28,8 @@ interface ArbitraryDataProvider {
fun deleteKeyForAccount(account: String, key: String)

fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: Long)

fun incrementValue(accountName: String, key: String)
fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: Boolean)
fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: String)

Expand All @@ -43,5 +45,7 @@ interface ArbitraryDataProvider {
const val DIRECT_EDITING = "DIRECT_EDITING"
const val DIRECT_EDITING_ETAG = "DIRECT_EDITING_ETAG"
const val PREDEFINED_STATUS = "PREDEFINED_STATUS"
const val E2E_ERRORS = "E2E_ERRORS"
const val E2E_ERRORS_TIMESTAMP = "E2E_ERRORS_TIMESTAMP"
}
}
Expand Up @@ -63,6 +63,17 @@ public void storeOrUpdateKeyValue(@NonNull String accountName, @NonNull String k
storeOrUpdateKeyValue(accountName, key, String.valueOf(newValue));
}

@Override
public void incrementValue(@NonNull String accountName, @NonNull String key) {
int oldValue = getIntegerValue(accountName, key);

int value = 1;
if (oldValue > 0) {
value = oldValue + 1;
}
storeOrUpdateKeyValue(accountName, key, value);
}

@Override
public void storeOrUpdateKeyValue(@NonNull final String accountName, @NonNull final String key, final boolean newValue) {
storeOrUpdateKeyValue(accountName, key, String.valueOf(newValue));
Expand Down
Expand Up @@ -1954,6 +1954,7 @@ private ContentValues createContentValues(String accountName, OCCapability capab
capability.getFilesLockingVersion());
contentValues.put(ProviderTableMeta.CAPABILITIES_GROUPFOLDERS, capability.getGroupfolders().getValue());
contentValues.put(ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT, capability.getDropAccount().getValue());
contentValues.put(ProviderTableMeta.CAPABILITIES_SECURITY_GUARD, capability.getSecurityGuard().getValue());

return contentValues;
}
Expand Down Expand Up @@ -2111,6 +2112,7 @@ private OCCapability createCapabilityInstance(Cursor cursor) {
getString(cursor, ProviderTableMeta.CAPABILITIES_FILES_LOCKING_VERSION));
capability.setGroupfolders(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_GROUPFOLDERS));
capability.setDropAccount(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT));
capability.setSecurityGuard(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_SECURITY_GUARD));
}
return capability;
}
Expand Down Expand Up @@ -2287,7 +2289,18 @@ public User getUser() {
return user;
}

public OCFile getDefaultRootPath(){
public OCFile getDefaultRootPath() {
return new OCFile(OCFile.ROOT_PATH);
}

public List<OCFile> getFilesWithSyncConflict(User user) {
List<FileEntity> fileEntities = fileDao.getFilesWithSyncConflict(user.getAccountName());
List<OCFile> files = new ArrayList<>(fileEntities.size());

for (FileEntity fileEntity : fileEntities) {
files.add(createFileInstance(fileEntity));
}

return files;
}
}

0 comments on commit d2726e5

Please sign in to comment.