Browse Source

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

Add info for background worker execution
Jonas Mayer 1 year ago
parent
commit
14a6d655d9

+ 1 - 1
app/src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt

@@ -104,7 +104,7 @@ class BackgroundJobManagerTest {
             clock = mock()
             whenever(clock.currentTime).thenReturn(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) {

+ 3 - 1
app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt

@@ -25,6 +25,7 @@ import android.Manifest
 import androidx.test.rule.GrantPermissionRule
 import androidx.work.WorkManager
 import com.nextcloud.client.core.ClockImpl
+import com.nextcloud.client.preferences.AppPreferencesImpl
 import com.nextcloud.test.RetryTestRule
 import com.owncloud.android.AbstractIT
 import com.owncloud.android.AbstractOnServerIT
@@ -43,7 +44,8 @@ import java.io.FileInputStream
 
 class ContactsBackupIT : AbstractOnServerIT() {
     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
     val writeContactsRule = GrantPermissionRule.grant(Manifest.permission.WRITE_CONTACTS)

+ 12 - 0
app/src/main/java/com/nextcloud/client/di/ComponentsModule.java

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

+ 70 - 4
app/src/main/java/com/nextcloud/client/etm/pages/EtmBackgroundJobsFragment.kt

@@ -20,6 +20,7 @@
  */
 package com.nextcloud.client.etm.pages
 
+import android.annotation.SuppressLint
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.Menu
@@ -32,15 +33,23 @@ import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.DividerItemDecoration
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.client.di.Injectable
 import com.nextcloud.client.etm.EtmBaseFragment
+import com.nextcloud.client.jobs.BackgroundJobManagerImpl
 import com.nextcloud.client.jobs.JobInfo
+import com.nextcloud.client.preferences.AppPreferences
 import com.owncloud.android.R
 import java.text.SimpleDateFormat
 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) {
             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 progress = view.findViewById<TextView>(R.id.etm_background_job_progress)
             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
                 get() {
@@ -63,6 +76,19 @@ class EtmBackgroundJobsFragment : EtmBaseFragment() {
                         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())
@@ -74,13 +100,20 @@ class EtmBackgroundJobsFragment : EtmBaseFragment() {
 
         override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
             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 {
             return backgroundJobs.size
         }
 
+        @SuppressLint("SetTextI18n")
         override fun onBindViewHolder(vh: ViewHolder, position: Int) {
             val info = backgroundJobs[position]
             vh.uuid.text = info.id.toString()
@@ -94,6 +127,34 @@ class EtmBackgroundJobsFragment : EtmBaseFragment() {
             } else {
                 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? {
         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.layoutManager = LinearLayoutManager(context)
         list.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
@@ -127,22 +188,27 @@ class EtmBackgroundJobsFragment : EtmBaseFragment() {
                 vm.cancelAllJobs()
                 true
             }
+
             R.id.etm_background_jobs_prune -> {
                 vm.pruneJobs()
                 true
             }
+
             R.id.etm_background_jobs_start_test -> {
                 vm.startTestJob(periodic = false)
                 true
             }
+
             R.id.etm_background_jobs_schedule_test -> {
                 vm.startTestJob(periodic = true)
                 true
             }
+
             R.id.etm_background_jobs_cancel_test -> {
                 vm.cancelTestJob()
                 true
             }
+
             else -> super.onOptionsItemSelected(item)
         }
     }

+ 15 - 3
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt

@@ -51,7 +51,7 @@ import javax.inject.Provider
  *
  * 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(
     private val logger: Logger,
     private val preferences: AppPreferences,
@@ -104,6 +104,7 @@ class BackgroundJobFactory @Inject constructor(
                 FilesUploadWorker::class -> createFilesUploadWorker(context, workerParameters)
                 GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters)
                 HealthStatusWork::class -> createHealthStatusWork(context, workerParameters)
+                TestJob::class -> createTestJob(context, workerParameters)
                 else -> null // caller falls back to default factory
             }
         }
@@ -183,7 +184,8 @@ class BackgroundJobFactory @Inject constructor(
             uploadsStorageManager = uploadsStorageManager,
             connectivityService = connectivityService,
             powerManagementService = powerManagementService,
-            syncedFolderProvider = syncedFolderProvider
+            syncedFolderProvider = syncedFolderProvider,
+            backgroundJobManager = backgroundJobManager.get()
         )
     }
 
@@ -245,6 +247,7 @@ class BackgroundJobFactory @Inject constructor(
             accountManager,
             viewThemeUtils.get(),
             localBroadcastManager.get(),
+            backgroundJobManager.get(),
             context,
             params
         )
@@ -267,7 +270,16 @@ class BackgroundJobFactory @Inject constructor(
             context,
             params,
             accountManager,
-            arbitraryDataProvider
+            arbitraryDataProvider,
+            backgroundJobManager.get()
+        )
+    }
+
+    private fun createTestJob(context: Context, params: WorkerParameters): TestJob {
+        return TestJob(
+            context,
+            params,
+            backgroundJobManager.get()
         )
     }
 }

+ 5 - 0
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt

@@ -20,6 +20,7 @@
 package com.nextcloud.client.jobs
 
 import androidx.lifecycle.LiveData
+import androidx.work.ListenableWorker
 import com.nextcloud.client.account.User
 import com.owncloud.android.datamodel.OCFile
 
@@ -35,6 +36,10 @@ interface BackgroundJobManager {
      */
     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
      * and launches synchronization when needed.

+ 54 - 7
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt

@@ -36,7 +36,9 @@ import androidx.work.WorkManager
 import androidx.work.workDataOf
 import com.nextcloud.client.account.User
 import com.nextcloud.client.core.Clock
+import com.nextcloud.client.di.Injectable
 import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
+import com.nextcloud.client.preferences.AppPreferences
 import com.owncloud.android.datamodel.OCFile
 import java.util.Date
 import java.util.UUID
@@ -60,10 +62,12 @@ import kotlin.reflect.KClass
 @Suppress("TooManyFunctions") // we expect this implementation to have rich API
 internal class BackgroundJobManagerImpl(
     private val workManager: WorkManager,
-    private val clock: Clock
-) : BackgroundJobManager {
+    private val clock: Clock,
+    private val preferences: AppPreferences
+) : BackgroundJobManager, Injectable {
 
     companion object {
+
         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_PERIODIC_CONTACTS_BACKUP = "periodic_contacts_backup"
@@ -82,6 +86,7 @@ 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"
 
@@ -91,13 +96,16 @@ internal class BackgroundJobManagerImpl(
 
         const val TAG_PREFIX_NAME = "name"
         const val TAG_PREFIX_USER = "user"
+        const val TAG_PREFIX_CLASS = "class"
         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 PERIODIC_BACKUP_INTERVAL_MINUTES = 24 * 60L
         const val DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES = 15L
         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 {
             return if (user == null) {
                 "$TAG_PREFIX_NAME:$name"
@@ -107,6 +115,7 @@ internal class BackgroundJobManagerImpl(
         }
 
         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 parseTag(tag: String): Pair<String, String>? {
@@ -120,11 +129,11 @@ internal class BackgroundJobManagerImpl(
         }
 
         fun parseTimestamp(timestamp: String): Date {
-            try {
+            return try {
                 val ms = timestamp.toLong()
-                return Date(ms)
+                Date(ms)
             } catch (ex: NumberFormatException) {
-                return Date(0)
+                Date(0)
             }
         }
 
@@ -143,12 +152,48 @@ internal class BackgroundJobManagerImpl(
                     name = metadata.get(TAG_PREFIX_NAME) ?: NOT_SET_VALUE,
                     user = metadata.get(TAG_PREFIX_USER) ?: NOT_SET_VALUE,
                     started = timestamp,
-                    progress = info.progress.getInt("progress", -1)
+                    progress = info.progress.getInt("progress", -1),
+                    workerClass = metadata.get(TAG_PREFIX_CLASS) ?: NOT_SET_VALUE
                 )
             } else {
                 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(formatNameTag(jobName, user))
             .addTag(formatTimeTag(clock.currentTime))
+            .addTag(formatClassTag(jobClass))
         user?.let { builder.addTag(formatUserTag(it)) }
         return builder
     }
@@ -187,6 +233,7 @@ internal class BackgroundJobManagerImpl(
             .addTag(TAG_ALL)
             .addTag(formatNameTag(jobName, user))
             .addTag(formatTimeTag(clock.currentTime))
+            .addTag(formatClassTag(jobClass))
         user?.let { builder.addTag(formatUserTag(it)) }
         return builder
     }

+ 10 - 1
app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt

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

+ 10 - 3
app/src/main/java/com/nextcloud/client/jobs/FilesSyncWork.kt

@@ -64,7 +64,8 @@ class FilesSyncWork(
     private val uploadsStorageManager: UploadsStorageManager,
     private val connectivityService: ConnectivityService,
     private val powerManagementService: PowerManagementService,
-    private val syncedFolderProvider: SyncedFolderProvider
+    private val syncedFolderProvider: SyncedFolderProvider,
+    private val backgroundJobManager: BackgroundJobManager
 ) : Worker(context, params) {
 
     companion object {
@@ -74,10 +75,14 @@ class FilesSyncWork(
     }
 
     override fun doWork(): Result {
+        backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
+
         val overridePowerSaving = inputData.getBoolean(OVERRIDE_POWER_SAVING, false)
         // If we are in power save mode, better to postpone upload
         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 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

+ 10 - 2
app/src/main/java/com/nextcloud/client/jobs/FilesUploadWorker.kt

@@ -69,6 +69,7 @@ class FilesUploadWorker(
     val userAccountManager: UserAccountManager,
     val viewThemeUtils: ViewThemeUtils,
     val localBroadcastManager: LocalBroadcastManager,
+    private val backgroundJobManager: BackgroundJobManager,
     val context: Context,
     params: WorkerParameters
 ) : Worker(context, params), OnDatatransferProgressListener {
@@ -80,10 +81,15 @@ class FilesUploadWorker(
     private val fileUploaderDelegate = FileUploaderDelegate()
 
     override fun doWork(): Result {
+        backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
+
         val accountName = inputData.getString(ACCOUNT)
         if (accountName.isNullOrEmpty()) {
             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")
-        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) {

+ 7 - 2
app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt

@@ -42,9 +42,12 @@ class HealthStatusWork(
     private val context: Context,
     params: WorkerParameters,
     private val userAccountManager: UserAccountManager,
-    private val arbitraryDataProvider: ArbitraryDataProvider
+    private val arbitraryDataProvider: ArbitraryDataProvider,
+    private val backgroundJobManager: BackgroundJobManager
 ) : Worker(context, params) {
     override fun doWork(): Result {
+        backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
+
         for (user in userAccountManager.allUsers) {
             // only if security guard is enabled
             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? {

+ 8 - 0
app/src/main/java/com/nextcloud/client/jobs/JobInfo.kt

@@ -27,6 +27,14 @@ data class JobInfo(
     val state: String = "",
     val name: String = "",
     val user: String = "",
+    val workerClass: String = "",
     val started: Date = Date(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
+)

+ 7 - 2
app/src/main/java/com/nextcloud/client/jobs/JobsModule.kt

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

+ 8 - 2
app/src/main/java/com/nextcloud/client/jobs/TestJob.kt

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

+ 9 - 0
app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java

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

+ 23 - 0
app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java

@@ -28,11 +28,13 @@ import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
 
+import com.google.common.reflect.TypeToken;
 import com.google.gson.Gson;
 import com.nextcloud.appReview.AppReviewShownModel;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.account.UserAccountManagerImpl;
+import com.nextcloud.client.jobs.LogEntry;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
 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.utils.FileSortOrder;
 
+import java.lang.reflect.Type;
+import java.util.List;
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArraySet;
 
@@ -49,6 +53,7 @@ import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 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}.
@@ -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__IN_APP_REVIEW_DATA = "in_app_review_data";
 
+    private static final String LOG_ENTRY = "log_entry";
+
     private final Context context;
     private final SharedPreferences preferences;
     private final UserAccountManager userAccountManager;
@@ -499,6 +506,22 @@ public final class AppPreferencesImpl implements AppPreferences {
         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
     public void setLastSeenVersionCode(int versionCode) {
         preferences.edit().putInt(AUTO_PREF__LAST_SEEN_VERSION_CODE, versionCode).apply();

+ 44 - 0
app/src/main/res/layout/etm_background_job_list_item.xml

@@ -136,4 +136,48 @@
 
     </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>

+ 2 - 1
app/src/main/res/values/strings.xml

@@ -878,8 +878,9 @@
     <string name="etm_background_job_name">Job name</string>
     <string name="etm_background_job_user">User</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_execution_count">Times run in 48h</string>
     <string name="etm_migrations">Migrations (app upgrade)</string>
     <string name="etm_transfer">File transfer</string>
     <string name="etm_transfer_remote_path">Remote path</string>