Skip to content

Commit

Permalink
Report client health
Browse files Browse the repository at this point in the history
Signed-off-by: tobiasKaminsky <[email protected]>
  • Loading branch information
tobiasKaminsky committed Dec 8, 2023
1 parent fa07aad commit 8b2fd2f
Show file tree
Hide file tree
Showing 27 changed files with 322 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ public boolean isPowerSavingExclusionAvailable() {
};

UserAccountManager accountManager = UserAccountManagerImpl.fromContext(targetContext);
UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(accountManager,
UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(accountManager.getUser(),
targetContext.getContentResolver());

UploadFileOperation newUpload = new UploadFileOperation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ public boolean isPowerSavingExclusionAvailable() {
};

UserAccountManager accountManager = UserAccountManagerImpl.fromContext(targetContext);
UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(accountManager,
UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(accountManager.getUser(),
targetContext.getContentResolver());

UploadFileOperation newUpload = new UploadFileOperation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public class UploadIT extends AbstractOnServerIT {
private static final String FOLDER = "/testUpload/";

private UploadsStorageManager uploadsStorageManager =
new UploadsStorageManager(UserAccountManagerImpl.fromContext(targetContext),
new UploadsStorageManager(UserAccountManagerImpl.fromContext(targetContext).getUser(),
targetContext.getContentResolver());

private ConnectivityService connectivityServiceMock = new ConnectivityService() {
Expand Down
Original file line number Diff line number Diff line change
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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public class UploadStorageManagerTest extends AbstractIT {
public void setUp() {
Context instrumentationCtx = ApplicationProvider.getApplicationContext();
ContentResolver contentResolver = instrumentationCtx.getContentResolver();
uploadsStorageManager = new UploadsStorageManager(currentAccountProvider, contentResolver);
uploadsStorageManager = new UploadsStorageManager(currentAccountProvider.getUser(), contentResolver);
userAccountManager = UserAccountManagerImpl.fromContext(targetContext);

Account temp = new Account("[email protected]", MainApp.getAccountType(targetContext));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
fun setUp() {
val contentResolver = targetContext.contentResolver
val accountManager: UserAccountManager = UserAccountManagerImpl.fromContext(targetContext)
uploadsStorageManager = UploadsStorageManager(accountManager, contentResolver)
uploadsStorageManager = UploadsStorageManager(accountManager.user, contentResolver)
}

/**
Expand Down
Original file line number Diff line number Diff line change
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>
}
Original file line number Diff line number Diff line change
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?
)
6 changes: 3 additions & 3 deletions app/src/main/java/com/nextcloud/client/di/AppModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,9 @@ FilesRepository filesRepository(UserAccountManager accountManager, ClientFactory
}

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

@Provides
Expand Down
Original file line number Diff line number Diff line change
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
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,6 @@ interface BackgroundJobManager {

fun pruneJobs()
fun cancelAllJobs()
fun schedulePeriodicHealthStatus()
fun startHealthStatus()
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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(user, 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
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@ public void onCreate() {
backgroundJobManager.scheduleMediaFoldersDetectionJob();
backgroundJobManager.startMediaFoldersDetectionJob();

backgroundJobManager.schedulePeriodicHealthStatus();

registerGlobalPassCodeProtection();
}

Expand Down
Original file line number Diff line number Diff line change
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"
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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;
}
}
Loading

0 comments on commit 8b2fd2f

Please sign in to comment.