Merge pull request #12306 from nextcloud/add-info-for-background-worker

Add info for background worker execution
This commit is contained in:
Jonas Mayer 2023-12-21 13:57:52 +01:00 committed by GitHub
commit 14a6d655d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 298 additions and 29 deletions

View file

@ -104,7 +104,7 @@ class BackgroundJobManagerTest {
clock = mock() clock = mock()
whenever(clock.currentTime).thenReturn(TIMESTAMP) whenever(clock.currentTime).thenReturn(TIMESTAMP)
whenever(clock.currentDate).thenReturn(Date(TIMESTAMP)) whenever(clock.currentDate).thenReturn(Date(TIMESTAMP))
backgroundJobManager = BackgroundJobManagerImpl(workManager, clock) backgroundJobManager = BackgroundJobManagerImpl(workManager, clock, mock())
} }
fun assertHasRequiredTags(tags: Set<String>, jobName: String, user: User? = null) { fun assertHasRequiredTags(tags: Set<String>, jobName: String, user: User? = null) {

View file

@ -25,6 +25,7 @@ import android.Manifest
import androidx.test.rule.GrantPermissionRule import androidx.test.rule.GrantPermissionRule
import androidx.work.WorkManager import androidx.work.WorkManager
import com.nextcloud.client.core.ClockImpl import com.nextcloud.client.core.ClockImpl
import com.nextcloud.client.preferences.AppPreferencesImpl
import com.nextcloud.test.RetryTestRule import com.nextcloud.test.RetryTestRule
import com.owncloud.android.AbstractIT import com.owncloud.android.AbstractIT
import com.owncloud.android.AbstractOnServerIT import com.owncloud.android.AbstractOnServerIT
@ -43,7 +44,8 @@ import java.io.FileInputStream
class ContactsBackupIT : AbstractOnServerIT() { class ContactsBackupIT : AbstractOnServerIT() {
val workmanager = WorkManager.getInstance(targetContext) val workmanager = WorkManager.getInstance(targetContext)
private val backgroundJobManager = BackgroundJobManagerImpl(workmanager, ClockImpl()) val preferences = AppPreferencesImpl.fromContext(targetContext)
private val backgroundJobManager = BackgroundJobManagerImpl(workmanager, ClockImpl(), preferences)
@get:Rule @get:Rule
val writeContactsRule = GrantPermissionRule.grant(Manifest.permission.WRITE_CONTACTS) val writeContactsRule = GrantPermissionRule.grant(Manifest.permission.WRITE_CONTACTS)

View file

@ -23,8 +23,11 @@ package com.nextcloud.client.di;
import com.nextcloud.client.documentscan.DocumentScanActivity; import com.nextcloud.client.documentscan.DocumentScanActivity;
import com.nextcloud.client.editimage.EditImageActivity; import com.nextcloud.client.editimage.EditImageActivity;
import com.nextcloud.client.etm.EtmActivity; import com.nextcloud.client.etm.EtmActivity;
import com.nextcloud.client.etm.pages.EtmBackgroundJobsFragment;
import com.nextcloud.client.files.downloader.FileTransferService; import com.nextcloud.client.files.downloader.FileTransferService;
import com.nextcloud.client.jobs.BackgroundJobManagerImpl;
import com.nextcloud.client.jobs.NotificationWork; import com.nextcloud.client.jobs.NotificationWork;
import com.nextcloud.client.jobs.TestJob;
import com.nextcloud.client.logger.ui.LogsActivity; import com.nextcloud.client.logger.ui.LogsActivity;
import com.nextcloud.client.logger.ui.LogsViewModel; import com.nextcloud.client.logger.ui.LogsViewModel;
import com.nextcloud.client.media.PlayerService; import com.nextcloud.client.media.PlayerService;
@ -478,4 +481,13 @@ abstract class ComponentsModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract ImageDetailFragment imageDetailFragment(); abstract ImageDetailFragment imageDetailFragment();
@ContributesAndroidInjector
abstract EtmBackgroundJobsFragment etmBackgroundJobsFragment();
@ContributesAndroidInjector
abstract BackgroundJobManagerImpl backgroundJobManagerImpl();
@ContributesAndroidInjector
abstract TestJob testJob();
} }

View file

@ -20,6 +20,7 @@
*/ */
package com.nextcloud.client.etm.pages package com.nextcloud.client.etm.pages
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
@ -32,15 +33,23 @@ import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.etm.EtmBaseFragment import com.nextcloud.client.etm.EtmBaseFragment
import com.nextcloud.client.jobs.BackgroundJobManagerImpl
import com.nextcloud.client.jobs.JobInfo import com.nextcloud.client.jobs.JobInfo
import com.nextcloud.client.preferences.AppPreferences
import com.owncloud.android.R import com.owncloud.android.R
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import javax.inject.Inject
class EtmBackgroundJobsFragment : EtmBaseFragment() { class EtmBackgroundJobsFragment : EtmBaseFragment(), Injectable {
class Adapter(private val inflater: LayoutInflater) : RecyclerView.Adapter<Adapter.ViewHolder>() { @Inject
lateinit var preferences: AppPreferences
class Adapter(private val inflater: LayoutInflater, private val preferences: AppPreferences) :
RecyclerView.Adapter<Adapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val uuid = view.findViewById<TextView>(R.id.etm_background_job_uuid) val uuid = view.findViewById<TextView>(R.id.etm_background_job_uuid)
@ -50,6 +59,10 @@ class EtmBackgroundJobsFragment : EtmBaseFragment() {
val started = view.findViewById<TextView>(R.id.etm_background_job_started) val started = view.findViewById<TextView>(R.id.etm_background_job_started)
val progress = view.findViewById<TextView>(R.id.etm_background_job_progress) val progress = view.findViewById<TextView>(R.id.etm_background_job_progress)
private val progressRow = view.findViewById<View>(R.id.etm_background_job_progress_row) private val progressRow = view.findViewById<View>(R.id.etm_background_job_progress_row)
val executionCount = view.findViewById<TextView>(R.id.etm_background_execution_count)
val executionLog = view.findViewById<TextView>(R.id.etm_background_execution_logs)
private val executionLogRow = view.findViewById<View>(R.id.etm_background_execution_logs_row)
val executionTimesRow = view.findViewById<View>(R.id.etm_background_execution_times_row)
var progressEnabled: Boolean = progressRow.visibility == View.VISIBLE var progressEnabled: Boolean = progressRow.visibility == View.VISIBLE
get() { get() {
@ -63,6 +76,19 @@ class EtmBackgroundJobsFragment : EtmBaseFragment() {
View.GONE View.GONE
} }
} }
var logsEnabled: Boolean = executionLogRow.visibility == View.VISIBLE
get() {
return executionLogRow.visibility == View.VISIBLE
}
set(value) {
field = value
executionLogRow.visibility = if (value) {
View.VISIBLE
} else {
View.GONE
}
}
} }
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:MM:ssZ", Locale.getDefault()) private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:MM:ssZ", Locale.getDefault())
@ -74,13 +100,20 @@ class EtmBackgroundJobsFragment : EtmBaseFragment() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = inflater.inflate(R.layout.etm_background_job_list_item, parent, false) val view = inflater.inflate(R.layout.etm_background_job_list_item, parent, false)
return ViewHolder(view) val viewHolder = ViewHolder(view)
viewHolder.logsEnabled = false
viewHolder.executionTimesRow.visibility = View.GONE
view.setOnClickListener {
viewHolder.logsEnabled = !viewHolder.logsEnabled
}
return viewHolder
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
return backgroundJobs.size return backgroundJobs.size
} }
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(vh: ViewHolder, position: Int) { override fun onBindViewHolder(vh: ViewHolder, position: Int) {
val info = backgroundJobs[position] val info = backgroundJobs[position]
vh.uuid.text = info.id.toString() vh.uuid.text = info.id.toString()
@ -94,6 +127,34 @@ class EtmBackgroundJobsFragment : EtmBaseFragment() {
} else { } else {
vh.progressEnabled = false vh.progressEnabled = false
} }
val logs = preferences.readLogEntry()
val logsForThisWorker =
logs.filter { BackgroundJobManagerImpl.parseTag(it.workerClass)?.second == info.workerClass }
if (logsForThisWorker.isNotEmpty()) {
vh.executionTimesRow.visibility = View.VISIBLE
vh.executionCount.text =
"${logsForThisWorker.filter { it.started != null }.size} " +
"(${logsForThisWorker.filter { it.finished != null }.size})"
var logText = "Worker Logs\n\n" +
"*** Does NOT differentiate between immediate or periodic kinds of Work! ***\n" +
"*** Times run in 48h: Times started (Times finished) ***\n"
logsForThisWorker.forEach {
logText += "----------------------\n"
logText += "Worker ${BackgroundJobManagerImpl.parseTag(it.workerClass)?.second}\n"
logText += if (it.started == null) {
"ENDED at\n${it.finished}\nWith result: ${it.result}\n"
} else {
"STARTED at\n${it.started}\n"
}
}
vh.executionLog.text = logText
} else {
vh.executionLog.text = "Worker Logs\n\n" +
"No Entries -> Maybe logging is not implemented for Worker or it has not run yet."
vh.executionCount.text = "0"
vh.executionTimesRow.visibility = View.GONE
}
} }
} }
@ -107,7 +168,7 @@ class EtmBackgroundJobsFragment : EtmBaseFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_etm_background_jobs, container, false) val view = inflater.inflate(R.layout.fragment_etm_background_jobs, container, false)
adapter = Adapter(inflater) adapter = Adapter(inflater, preferences)
list = view.findViewById(R.id.etm_background_jobs_list) list = view.findViewById(R.id.etm_background_jobs_list)
list.layoutManager = LinearLayoutManager(context) list.layoutManager = LinearLayoutManager(context)
list.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) list.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
@ -127,22 +188,27 @@ class EtmBackgroundJobsFragment : EtmBaseFragment() {
vm.cancelAllJobs() vm.cancelAllJobs()
true true
} }
R.id.etm_background_jobs_prune -> { R.id.etm_background_jobs_prune -> {
vm.pruneJobs() vm.pruneJobs()
true true
} }
R.id.etm_background_jobs_start_test -> { R.id.etm_background_jobs_start_test -> {
vm.startTestJob(periodic = false) vm.startTestJob(periodic = false)
true true
} }
R.id.etm_background_jobs_schedule_test -> { R.id.etm_background_jobs_schedule_test -> {
vm.startTestJob(periodic = true) vm.startTestJob(periodic = true)
true true
} }
R.id.etm_background_jobs_cancel_test -> { R.id.etm_background_jobs_cancel_test -> {
vm.cancelTestJob() vm.cancelTestJob()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }

View file

@ -51,7 +51,7 @@ import javax.inject.Provider
* *
* This class is doing too many things and should be split up into smaller factories. * This class is doing too many things and should be split up into smaller factories.
*/ */
@Suppress("LongParameterList") // satisfied by DI @Suppress("LongParameterList", "TooManyFunctions") // satisfied by DI
class BackgroundJobFactory @Inject constructor( class BackgroundJobFactory @Inject constructor(
private val logger: Logger, private val logger: Logger,
private val preferences: AppPreferences, private val preferences: AppPreferences,
@ -104,6 +104,7 @@ class BackgroundJobFactory @Inject constructor(
FilesUploadWorker::class -> createFilesUploadWorker(context, workerParameters) FilesUploadWorker::class -> createFilesUploadWorker(context, workerParameters)
GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters) GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters)
HealthStatusWork::class -> createHealthStatusWork(context, workerParameters) HealthStatusWork::class -> createHealthStatusWork(context, workerParameters)
TestJob::class -> createTestJob(context, workerParameters)
else -> null // caller falls back to default factory else -> null // caller falls back to default factory
} }
} }
@ -183,7 +184,8 @@ class BackgroundJobFactory @Inject constructor(
uploadsStorageManager = uploadsStorageManager, uploadsStorageManager = uploadsStorageManager,
connectivityService = connectivityService, connectivityService = connectivityService,
powerManagementService = powerManagementService, powerManagementService = powerManagementService,
syncedFolderProvider = syncedFolderProvider syncedFolderProvider = syncedFolderProvider,
backgroundJobManager = backgroundJobManager.get()
) )
} }
@ -245,6 +247,7 @@ class BackgroundJobFactory @Inject constructor(
accountManager, accountManager,
viewThemeUtils.get(), viewThemeUtils.get(),
localBroadcastManager.get(), localBroadcastManager.get(),
backgroundJobManager.get(),
context, context,
params params
) )
@ -267,7 +270,16 @@ class BackgroundJobFactory @Inject constructor(
context, context,
params, params,
accountManager, accountManager,
arbitraryDataProvider arbitraryDataProvider,
backgroundJobManager.get()
)
}
private fun createTestJob(context: Context, params: WorkerParameters): TestJob {
return TestJob(
context,
params,
backgroundJobManager.get()
) )
} }
} }

View file

@ -20,6 +20,7 @@
package com.nextcloud.client.jobs package com.nextcloud.client.jobs
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.work.ListenableWorker
import com.nextcloud.client.account.User import com.nextcloud.client.account.User
import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.OCFile
@ -35,6 +36,10 @@ interface BackgroundJobManager {
*/ */
val jobs: LiveData<List<JobInfo>> val jobs: LiveData<List<JobInfo>>
fun logStartOfWorker(workerName: String?)
fun logEndOfWorker(workerName: String?, result: ListenableWorker.Result)
/** /**
* Start content observer job that monitors changes in media folders * Start content observer job that monitors changes in media folders
* and launches synchronization when needed. * and launches synchronization when needed.

View file

@ -36,7 +36,9 @@ import androidx.work.WorkManager
import androidx.work.workDataOf import androidx.work.workDataOf
import com.nextcloud.client.account.User import com.nextcloud.client.account.User
import com.nextcloud.client.core.Clock import com.nextcloud.client.core.Clock
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
import com.nextcloud.client.preferences.AppPreferences
import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.OCFile
import java.util.Date import java.util.Date
import java.util.UUID import java.util.UUID
@ -60,10 +62,12 @@ import kotlin.reflect.KClass
@Suppress("TooManyFunctions") // we expect this implementation to have rich API @Suppress("TooManyFunctions") // we expect this implementation to have rich API
internal class BackgroundJobManagerImpl( internal class BackgroundJobManagerImpl(
private val workManager: WorkManager, private val workManager: WorkManager,
private val clock: Clock private val clock: Clock,
) : BackgroundJobManager { private val preferences: AppPreferences
) : BackgroundJobManager, Injectable {
companion object { companion object {
const val TAG_ALL = "*" // This tag allows us to retrieve list of all jobs run by Nextcloud client const val TAG_ALL = "*" // This tag allows us to retrieve list of all jobs run by Nextcloud client
const val JOB_CONTENT_OBSERVER = "content_observer" const val JOB_CONTENT_OBSERVER = "content_observer"
const val JOB_PERIODIC_CONTACTS_BACKUP = "periodic_contacts_backup" const val JOB_PERIODIC_CONTACTS_BACKUP = "periodic_contacts_backup"
@ -82,6 +86,7 @@ internal class BackgroundJobManagerImpl(
const val JOB_PDF_GENERATION = "pdf_generation" const val JOB_PDF_GENERATION = "pdf_generation"
const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup" const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup"
const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export" const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export"
const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status" const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status"
const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status" const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status"
@ -91,13 +96,16 @@ internal class BackgroundJobManagerImpl(
const val TAG_PREFIX_NAME = "name" const val TAG_PREFIX_NAME = "name"
const val TAG_PREFIX_USER = "user" const val TAG_PREFIX_USER = "user"
const val TAG_PREFIX_CLASS = "class"
const val TAG_PREFIX_START_TIMESTAMP = "timestamp" const val TAG_PREFIX_START_TIMESTAMP = "timestamp"
val PREFIXES = setOf(TAG_PREFIX_NAME, TAG_PREFIX_USER, TAG_PREFIX_START_TIMESTAMP) val PREFIXES = setOf(TAG_PREFIX_NAME, TAG_PREFIX_USER, TAG_PREFIX_START_TIMESTAMP, TAG_PREFIX_CLASS)
const val NOT_SET_VALUE = "not set" const val NOT_SET_VALUE = "not set"
const val PERIODIC_BACKUP_INTERVAL_MINUTES = 24 * 60L const val PERIODIC_BACKUP_INTERVAL_MINUTES = 24 * 60L
const val DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES = 15L const val DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES = 15L
const val DEFAULT_IMMEDIATE_JOB_DELAY_SEC = 3L const val DEFAULT_IMMEDIATE_JOB_DELAY_SEC = 3L
private const val KEEP_LOG_MILLIS = 1000 * 60 * 60 * 24 * 3L
fun formatNameTag(name: String, user: User? = null): String { fun formatNameTag(name: String, user: User? = null): String {
return if (user == null) { return if (user == null) {
"$TAG_PREFIX_NAME:$name" "$TAG_PREFIX_NAME:$name"
@ -107,6 +115,7 @@ internal class BackgroundJobManagerImpl(
} }
fun formatUserTag(user: User): String = "$TAG_PREFIX_USER:${user.accountName}" fun formatUserTag(user: User): String = "$TAG_PREFIX_USER:${user.accountName}"
fun formatClassTag(jobClass: KClass<out ListenableWorker>): String = "$TAG_PREFIX_CLASS:${jobClass.simpleName}"
fun formatTimeTag(startTimestamp: Long): String = "$TAG_PREFIX_START_TIMESTAMP:$startTimestamp" fun formatTimeTag(startTimestamp: Long): String = "$TAG_PREFIX_START_TIMESTAMP:$startTimestamp"
fun parseTag(tag: String): Pair<String, String>? { fun parseTag(tag: String): Pair<String, String>? {
@ -120,11 +129,11 @@ internal class BackgroundJobManagerImpl(
} }
fun parseTimestamp(timestamp: String): Date { fun parseTimestamp(timestamp: String): Date {
try { return try {
val ms = timestamp.toLong() val ms = timestamp.toLong()
return Date(ms) Date(ms)
} catch (ex: NumberFormatException) { } catch (ex: NumberFormatException) {
return Date(0) Date(0)
} }
} }
@ -143,12 +152,48 @@ internal class BackgroundJobManagerImpl(
name = metadata.get(TAG_PREFIX_NAME) ?: NOT_SET_VALUE, name = metadata.get(TAG_PREFIX_NAME) ?: NOT_SET_VALUE,
user = metadata.get(TAG_PREFIX_USER) ?: NOT_SET_VALUE, user = metadata.get(TAG_PREFIX_USER) ?: NOT_SET_VALUE,
started = timestamp, started = timestamp,
progress = info.progress.getInt("progress", -1) progress = info.progress.getInt("progress", -1),
workerClass = metadata.get(TAG_PREFIX_CLASS) ?: NOT_SET_VALUE
) )
} else { } else {
null null
} }
} }
fun deleteOldLogs(logEntries: MutableList<LogEntry>): MutableList<LogEntry> {
logEntries.removeIf {
return@removeIf (
it.started != null &&
Date(Date().time - KEEP_LOG_MILLIS).after(it.started)
) ||
(
it.finished != null &&
Date(Date().time - KEEP_LOG_MILLIS).after(it.finished)
)
}
return logEntries
}
}
override fun logStartOfWorker(workerName: String?) {
val logs = deleteOldLogs(preferences.readLogEntry().toMutableList())
if (workerName == null) {
logs.add(LogEntry(Date(), null, null, NOT_SET_VALUE))
} else {
logs.add(LogEntry(Date(), null, null, workerName))
}
preferences.saveLogEntry(logs)
}
override fun logEndOfWorker(workerName: String?, result: ListenableWorker.Result) {
val logs = deleteOldLogs(preferences.readLogEntry().toMutableList())
if (workerName == null) {
logs.add(LogEntry(null, Date(), result.toString(), NOT_SET_VALUE))
} else {
logs.add(LogEntry(null, Date(), result.toString(), workerName))
}
preferences.saveLogEntry(logs)
} }
/** /**
@ -163,6 +208,7 @@ internal class BackgroundJobManagerImpl(
.addTag(TAG_ALL) .addTag(TAG_ALL)
.addTag(formatNameTag(jobName, user)) .addTag(formatNameTag(jobName, user))
.addTag(formatTimeTag(clock.currentTime)) .addTag(formatTimeTag(clock.currentTime))
.addTag(formatClassTag(jobClass))
user?.let { builder.addTag(formatUserTag(it)) } user?.let { builder.addTag(formatUserTag(it)) }
return builder return builder
} }
@ -187,6 +233,7 @@ internal class BackgroundJobManagerImpl(
.addTag(TAG_ALL) .addTag(TAG_ALL)
.addTag(formatNameTag(jobName, user)) .addTag(formatNameTag(jobName, user))
.addTag(formatTimeTag(clock.currentTime)) .addTag(formatTimeTag(clock.currentTime))
.addTag(formatClassTag(jobClass))
user?.let { builder.addTag(formatUserTag(it)) } user?.let { builder.addTag(formatUserTag(it)) }
return builder return builder
} }

View file

@ -41,12 +41,17 @@ class ContentObserverWork(
) : Worker(appContext, params) { ) : Worker(appContext, params) {
override fun doWork(): Result { override fun doWork(): Result {
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
if (params.triggeredContentUris.size > 0) { if (params.triggeredContentUris.size > 0) {
checkAndStartFileSyncJob() checkAndStartFileSyncJob()
backgroundJobManager.startMediaFoldersDetectionJob() backgroundJobManager.startMediaFoldersDetectionJob()
} }
recheduleSelf() recheduleSelf()
return Result.success()
val result = Result.success()
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
return result
} }
private fun recheduleSelf() { private fun recheduleSelf() {
@ -59,4 +64,8 @@ class ContentObserverWork(
backgroundJobManager.startImmediateFilesSyncJob(true, false) backgroundJobManager.startImmediateFilesSyncJob(true, false)
} }
} }
companion object {
val TAG: String = ContentObserverWork::class.java.simpleName
}
} }

View file

@ -64,7 +64,8 @@ class FilesSyncWork(
private val uploadsStorageManager: UploadsStorageManager, private val uploadsStorageManager: UploadsStorageManager,
private val connectivityService: ConnectivityService, private val connectivityService: ConnectivityService,
private val powerManagementService: PowerManagementService, private val powerManagementService: PowerManagementService,
private val syncedFolderProvider: SyncedFolderProvider private val syncedFolderProvider: SyncedFolderProvider,
private val backgroundJobManager: BackgroundJobManager
) : Worker(context, params) { ) : Worker(context, params) {
companion object { companion object {
@ -74,10 +75,14 @@ class FilesSyncWork(
} }
override fun doWork(): Result { override fun doWork(): Result {
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
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
if (powerManagementService.isPowerSavingEnabled && !overridePowerSaving) { if (powerManagementService.isPowerSavingEnabled && !overridePowerSaving) {
return Result.success() val result = Result.success()
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
return result
} }
val resources = context.resources val resources = context.resources
val lightVersion = resources.getBoolean(R.bool.syncedFolder_light) val lightVersion = resources.getBoolean(R.bool.syncedFolder_light)
@ -107,7 +112,9 @@ class FilesSyncWork(
) )
} }
} }
return Result.success() val result = Result.success()
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
return result
} }
@Suppress("LongMethod") // legacy code @Suppress("LongMethod") // legacy code

View file

@ -69,6 +69,7 @@ class FilesUploadWorker(
val userAccountManager: UserAccountManager, val userAccountManager: UserAccountManager,
val viewThemeUtils: ViewThemeUtils, val viewThemeUtils: ViewThemeUtils,
val localBroadcastManager: LocalBroadcastManager, val localBroadcastManager: LocalBroadcastManager,
private val backgroundJobManager: BackgroundJobManager,
val context: Context, val context: Context,
params: WorkerParameters params: WorkerParameters
) : Worker(context, params), OnDatatransferProgressListener { ) : Worker(context, params), OnDatatransferProgressListener {
@ -80,10 +81,15 @@ class FilesUploadWorker(
private val fileUploaderDelegate = FileUploaderDelegate() private val fileUploaderDelegate = FileUploaderDelegate()
override fun doWork(): Result { override fun doWork(): Result {
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
val accountName = inputData.getString(ACCOUNT) val accountName = inputData.getString(ACCOUNT)
if (accountName.isNullOrEmpty()) { if (accountName.isNullOrEmpty()) {
Log_OC.w(TAG, "User was null for file upload worker") Log_OC.w(TAG, "User was null for file upload worker")
return Result.failure() // user account is needed
val result = Result.failure()
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
return result // user account is needed
} }
/* /*
@ -100,7 +106,9 @@ class FilesUploadWorker(
} }
Log_OC.d(TAG, "No more pending uploads for account $accountName, stopping work") Log_OC.d(TAG, "No more pending uploads for account $accountName, stopping work")
return Result.success() val result = Result.success()
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
return result // user account is needed
} }
private fun handlePendingUploads(uploads: List<OCUpload>, accountName: String) { private fun handlePendingUploads(uploads: List<OCUpload>, accountName: String) {

View file

@ -42,9 +42,12 @@ class HealthStatusWork(
private val context: Context, private val context: Context,
params: WorkerParameters, params: WorkerParameters,
private val userAccountManager: UserAccountManager, private val userAccountManager: UserAccountManager,
private val arbitraryDataProvider: ArbitraryDataProvider private val arbitraryDataProvider: ArbitraryDataProvider,
private val backgroundJobManager: BackgroundJobManager
) : Worker(context, params) { ) : Worker(context, params) {
override fun doWork(): Result { override fun doWork(): Result {
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
for (user in userAccountManager.allUsers) { for (user in userAccountManager.allUsers) {
// only if security guard is enabled // only if security guard is enabled
if (!CapabilityUtils.getCapability(user, context).securityGuard.isTrue) { if (!CapabilityUtils.getCapability(user, context).securityGuard.isTrue) {
@ -92,7 +95,9 @@ class HealthStatusWork(
} }
} }
return Result.success() val result = Result.success()
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
return result
} }
private fun collectSyncConflicts(user: User): Problem? { private fun collectSyncConflicts(user: User): Problem? {

View file

@ -27,6 +27,14 @@ data class JobInfo(
val state: String = "", val state: String = "",
val name: String = "", val name: String = "",
val user: String = "", val user: String = "",
val workerClass: String = "",
val started: Date = Date(0), val started: Date = Date(0),
val progress: Int = 0 val progress: Int = 0
) )
data class LogEntry(
val started: Date? = null,
val finished: Date? = null,
val result: String? = null,
var workerClass: String = BackgroundJobManagerImpl.NOT_SET_VALUE
)

View file

@ -24,6 +24,7 @@ import android.content.ContextWrapper
import androidx.work.Configuration import androidx.work.Configuration
import androidx.work.WorkManager import androidx.work.WorkManager
import com.nextcloud.client.core.Clock import com.nextcloud.client.core.Clock
import com.nextcloud.client.preferences.AppPreferences
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import javax.inject.Singleton import javax.inject.Singleton
@ -50,7 +51,11 @@ class JobsModule {
@Provides @Provides
@Singleton @Singleton
fun backgroundJobManager(workManager: WorkManager, clock: Clock): BackgroundJobManager { fun backgroundJobManager(
return BackgroundJobManagerImpl(workManager, clock) workManager: WorkManager,
clock: Clock,
preferences: AppPreferences
): BackgroundJobManager {
return BackgroundJobManagerImpl(workManager, clock, preferences)
} }
} }

View file

@ -26,7 +26,8 @@ import androidx.work.WorkerParameters
class TestJob( class TestJob(
appContext: Context, appContext: Context,
params: WorkerParameters params: WorkerParameters,
private val backgroundJobManager: BackgroundJobManager
) : Worker(appContext, params) { ) : Worker(appContext, params) {
companion object { companion object {
@ -36,6 +37,8 @@ class TestJob(
} }
override fun doWork(): Result { override fun doWork(): Result {
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
for (i in 0..MAX_PROGRESS) { for (i in 0..MAX_PROGRESS) {
Thread.sleep(DELAY_MS) Thread.sleep(DELAY_MS)
val progress = Data.Builder() val progress = Data.Builder()
@ -43,6 +46,9 @@ class TestJob(
.build() .build()
setProgressAsync(progress) setProgressAsync(progress)
} }
return Result.success()
val result = Result.success()
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
return result
} }
} }

View file

@ -23,9 +23,12 @@
package com.nextcloud.client.preferences; package com.nextcloud.client.preferences;
import com.nextcloud.appReview.AppReviewShownModel; import com.nextcloud.appReview.AppReviewShownModel;
import com.nextcloud.client.jobs.LogEntry;
import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.utils.FileSortOrder; import com.owncloud.android.utils.FileSortOrder;
import java.util.List;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -317,6 +320,12 @@ public interface AppPreferences {
*/ */
int getLastSeenVersionCode(); int getLastSeenVersionCode();
void saveLogEntry(List<LogEntry> logEntryList);
List<LogEntry> readLogEntry();
/** /**
* Saves the version code as the last seen version code. * Saves the version code as the last seen version code.
* *

View file

@ -28,11 +28,13 @@ import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration; import android.content.res.Configuration;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.nextcloud.appReview.AppReviewShownModel; import com.nextcloud.appReview.AppReviewShownModel;
import com.nextcloud.client.account.User; import com.nextcloud.client.account.User;
import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.account.UserAccountManagerImpl; import com.nextcloud.client.account.UserAccountManagerImpl;
import com.nextcloud.client.jobs.LogEntry;
import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.FileDataStorageManager;
@ -41,6 +43,8 @@ import com.owncloud.android.ui.activity.PassCodeActivity;
import com.owncloud.android.ui.activity.SettingsActivity; import com.owncloud.android.ui.activity.SettingsActivity;
import com.owncloud.android.utils.FileSortOrder; import com.owncloud.android.utils.FileSortOrder;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
@ -49,6 +53,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import static com.owncloud.android.ui.fragment.OCFileListFragment.FOLDER_LAYOUT_LIST; import static com.owncloud.android.ui.fragment.OCFileListFragment.FOLDER_LAYOUT_LIST;
import static java.util.Collections.emptyList;
/** /**
* Implementation of application-wide preferences using {@link SharedPreferences}. * Implementation of application-wide preferences using {@link SharedPreferences}.
@ -108,6 +113,8 @@ public final class AppPreferencesImpl implements AppPreferences {
private static final String PREF__STORAGE_PERMISSION_REQUESTED = "storage_permission_requested"; private static final String PREF__STORAGE_PERMISSION_REQUESTED = "storage_permission_requested";
private static final String PREF__IN_APP_REVIEW_DATA = "in_app_review_data"; private static final String PREF__IN_APP_REVIEW_DATA = "in_app_review_data";
private static final String LOG_ENTRY = "log_entry";
private final Context context; private final Context context;
private final SharedPreferences preferences; private final SharedPreferences preferences;
private final UserAccountManager userAccountManager; private final UserAccountManager userAccountManager;
@ -499,6 +506,22 @@ public final class AppPreferencesImpl implements AppPreferences {
return preferences.getInt(AUTO_PREF__LAST_SEEN_VERSION_CODE, 0); return preferences.getInt(AUTO_PREF__LAST_SEEN_VERSION_CODE, 0);
} }
@Override
public void saveLogEntry(List<LogEntry> logEntryList) {
Gson gson = new Gson();
String json = gson.toJson(logEntryList);
preferences.edit().putString(LOG_ENTRY, json).apply();
}
@Override
public List<LogEntry> readLogEntry() {
String json = preferences.getString(LOG_ENTRY, null);
if (json == null) return emptyList();
Gson gson = new Gson();
Type listType = new TypeToken<List<LogEntry>>() {}.getType();
return gson.fromJson(json, listType);
}
@Override @Override
public void setLastSeenVersionCode(int versionCode) { public void setLastSeenVersionCode(int versionCode) {
preferences.edit().putInt(AUTO_PREF__LAST_SEEN_VERSION_CODE, versionCode).apply(); preferences.edit().putInt(AUTO_PREF__LAST_SEEN_VERSION_CODE, versionCode).apply();

View file

@ -136,4 +136,48 @@
</TableRow> </TableRow>
<TableRow
android:id="@+id/etm_background_execution_times_row"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:text="@string/etm_background_execution_count" />
<TextView
android:id="@+id/etm_background_execution_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="0" />
</TableRow>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TableRow
android:id="@+id/etm_background_execution_logs_row"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fadeScrollbars="false"
android:scrollbars="horizontal"
android:scrollHorizontally="true">
<TextView
android:id="@+id/etm_background_execution_logs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_span="2" />
</TableRow>
</HorizontalScrollView>
</TableLayout> </TableLayout>

View file

@ -878,8 +878,9 @@
<string name="etm_background_job_name">Job name</string> <string name="etm_background_job_name">Job name</string>
<string name="etm_background_job_user">User</string> <string name="etm_background_job_user">User</string>
<string name="etm_background_job_state">State</string> <string name="etm_background_job_state">State</string>
<string name="etm_background_job_started">Started</string> <string name="etm_background_job_started">Created</string>
<string name="etm_background_job_progress">Progress</string> <string name="etm_background_job_progress">Progress</string>
<string name="etm_background_execution_count">Times run in 48h</string>
<string name="etm_migrations">Migrations (app upgrade)</string> <string name="etm_migrations">Migrations (app upgrade)</string>
<string name="etm_transfer">File transfer</string> <string name="etm_transfer">File transfer</string>
<string name="etm_transfer_remote_path">Remote path</string> <string name="etm_transfer_remote_path">Remote path</string>