Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable-3.27] Report client health #12272

Merged
merged 1 commit into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,179 changes: 1,179 additions & 0 deletions app/schemas/com.nextcloud.client.database.NextcloudDatabase/76.json

Large diffs are not rendered by default.

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 @@ -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
Original file line number Diff line number Diff line change
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
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?
)
4 changes: 2 additions & 2 deletions app/src/main/java/com/nextcloud/client/di/AppModule.java
Original file line number Diff line number Diff line change
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
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(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
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
Loading