diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index d08f4181afe3..e9994c370069 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -24,14 +24,18 @@ jobs: run: | if [ -z "$GITHUB_HEAD_REF" ]; then # push - echo "branch=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT" - echo "pr=$GITHUB_RUN_ID" >> "$GITHUB_OUTPUT" - echo "repo=${{ github.repository }}" >> "$GITHUB_OUTPUT" + { + echo "branch=$GITHUB_REF_NAME" + echo "pr=$GITHUB_RUN_ID" + echo "repo=${{ github.repository }}" + } >> "$GITHUB_OUTPUT" else # pull request - echo "branch=$GITHUB_HEAD_REF" >> "$GITHUB_OUTPUT" - echo "pr=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" - echo "repo=${{ github.event.pull_request.head.repo.full_name }}" >> "$GITHUB_OUTPUT" + { + echo "branch=$GITHUB_HEAD_REF" + echo "pr=${{ github.event.pull_request.number }}" + echo "repo=${{ github.event.pull_request.head.repo.full_name }}" + } >> "$GITHUB_OUTPUT" fi - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: @@ -49,6 +53,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - mkdir -p $HOME/.gradle - echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > $HOME/.gradle/gradle.properties - scripts/analysis/analysis-wrapper.sh ${{ steps.get-vars.outputs.branch }} ${{ secrets.LOG_USERNAME }} ${{ secrets.LOG_PASSWORD }} $GITHUB_RUN_NUMBER ${{ steps.get-vars.outputs.pr }} + mkdir -p "$HOME/.gradle" + echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties" + scripts/analysis/analysis-wrapper.sh ${{ steps.get-vars.outputs.branch }} ${{ secrets.LOG_USERNAME }} ${{ secrets.LOG_PASSWORD }} "$GITHUB_RUN_NUMBER" ${{ steps.get-vars.outputs.pr }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 776b467f91db..12891d918d33 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -26,3 +26,4 @@ jobs: Please take a look again and update the issue with new details, otherwise the issue will be automatically closed in 2 weeks. Thank you! exempt-all-pr-milestones: true + labels-to-remove-when-unstale: 'needs info' diff --git a/app/src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt b/app/src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt index e06adbecbce9..28560deb9615 100644 --- a/app/src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt +++ b/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, jobName: String, user: User? = null) { diff --git a/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt b/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt index c4235d553f5c..5ff30f3af6bb 100644 --- a/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt +++ b/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) diff --git a/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt b/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt index f7fd21b1e075..704dfd9ce2ee 100644 --- a/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt @@ -139,12 +139,10 @@ abstract class FileUploaderIT : AbstractOnServerIT() { val file = getDummyFile("chunkedFile.txt") FileUploader.uploadNewFile( - targetContext, user, file.absolutePath, "/testFile.txt", FileUploader.LOCAL_BEHAVIOUR_COPY, - null, true, UploadFileOperation.CREATED_BY_USER, false, @@ -258,12 +256,10 @@ abstract class FileUploaderIT : AbstractOnServerIT() { val file = getDummyFile("nonEmpty.txt") FileUploader.uploadNewFile( - targetContext, user, file.absolutePath, "/testFile.txt", FileUploader.LOCAL_BEHAVIOUR_COPY, - null, true, UploadFileOperation.CREATED_BY_USER, false, @@ -369,12 +365,10 @@ abstract class FileUploaderIT : AbstractOnServerIT() { val file = getDummyFile("chunkedFile.txt") FileUploader.uploadNewFile( - targetContext, user, file.absolutePath, "/testFile.txt", FileUploader.LOCAL_BEHAVIOUR_COPY, - null, true, UploadFileOperation.CREATED_BY_USER, false, @@ -476,12 +470,10 @@ abstract class FileUploaderIT : AbstractOnServerIT() { val file = getDummyFile("chunkedFile.txt") FileUploader.uploadNewFile( - targetContext, user, file.absolutePath, "/testFile.txt", FileUploader.LOCAL_BEHAVIOUR_COPY, - null, true, UploadFileOperation.CREATED_BY_USER, false, diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java index 82f9c8738108..83924bdb8f26 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -23,8 +23,11 @@ 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(); } diff --git a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt b/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt index 31e5c348194e..b7dda52d4254 100644 --- a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt @@ -36,7 +36,6 @@ import com.owncloud.android.files.services.FileUploader import com.owncloud.android.files.services.NameCollisionPolicy import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.helpers.FileOperationsHelper -import com.owncloud.android.utils.MimeType import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -184,16 +183,10 @@ class DocumentScanViewModel @Inject constructor( uploadFolder + OCFile.PATH_SEPARATOR + File(it).name }.toTypedArray() - val mimetypes = pageList.map { - MimeType.JPEG - }.toTypedArray() - FileUploader.uploadNewFile( - getApplication(), currentAccountProvider.user, pageList.toTypedArray(), uploadPaths, - mimetypes, FileUploader.LOCAL_BEHAVIOUR_DELETE, true, UploadFileOperation.CREATED_BY_USER, diff --git a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt b/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt index 5fd4a640d079..ffbd5b5caf53 100644 --- a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt +++ b/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt @@ -39,7 +39,6 @@ import com.owncloud.android.files.services.FileUploader import com.owncloud.android.files.services.NameCollisionPolicy import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.notifications.NotificationUtils -import com.owncloud.android.utils.MimeType import com.owncloud.android.utils.theme.ViewThemeUtils import java.io.File import java.security.SecureRandom @@ -124,12 +123,10 @@ class GeneratePdfFromImagesWork( val uploadPath = uploadFolder + OCFile.PATH_SEPARATOR + File(pdfPath).name FileUploader.uploadNewFile( - appContext, user, pdfPath, uploadPath, FileUploader.LOCAL_BEHAVIOUR_DELETE, // MIME type will be detected from file name - MimeType.PDF, true, UploadFileOperation.CREATED_BY_USER, false, diff --git a/app/src/main/java/com/nextcloud/client/etm/pages/EtmBackgroundJobsFragment.kt b/app/src/main/java/com/nextcloud/client/etm/pages/EtmBackgroundJobsFragment.kt index 714c018e5942..d55caec0b0ae 100644 --- a/app/src/main/java/com/nextcloud/client/etm/pages/EtmBackgroundJobsFragment.kt +++ b/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() { + @Inject + lateinit var preferences: AppPreferences + + class Adapter(private val inflater: LayoutInflater, private val preferences: AppPreferences) : + RecyclerView.Adapter() { class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val uuid = view.findViewById(R.id.etm_background_job_uuid) @@ -50,6 +59,10 @@ class EtmBackgroundJobsFragment : EtmBaseFragment() { val started = view.findViewById(R.id.etm_background_job_started) val progress = view.findViewById(R.id.etm_background_job_progress) private val progressRow = view.findViewById(R.id.etm_background_job_progress_row) + val executionCount = view.findViewById(R.id.etm_background_execution_count) + val executionLog = view.findViewById(R.id.etm_background_execution_logs) + private val executionLogRow = view.findViewById(R.id.etm_background_execution_logs_row) + val executionTimesRow = view.findViewById(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) } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index 1fb333f5ce37..cc8c999dcabb 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/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() ) } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index cc8bc6a52f69..750e967ccea9 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/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> + 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. diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 7e954a0b29db..68bf01dcda36 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/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): String = "$TAG_PREFIX_CLASS:${jobClass.simpleName}" fun formatTimeTag(startTimestamp: Long): String = "$TAG_PREFIX_START_TIMESTAMP:$startTimestamp" fun parseTag(tag: String): Pair? { @@ -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): MutableList { + 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 } diff --git a/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt b/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt index 2cab7d17b387..4e9840ccef35 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt +++ b/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 + } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/FilesSyncWork.kt b/app/src/main/java/com/nextcloud/client/jobs/FilesSyncWork.kt index cc503a2ad14d..8570240ba2fd 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/FilesSyncWork.kt +++ b/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 @@ -155,7 +162,6 @@ class FilesSyncWork( } val localPaths = pathsAndMimes.map { it.first }.toTypedArray() val remotePaths = pathsAndMimes.map { it.second }.toTypedArray() - val mimetypes = pathsAndMimes.map { it.third }.toTypedArray() if (lightVersion) { needsCharging = resources.getBoolean(R.bool.syncedFolder_light_on_charging) @@ -170,12 +176,11 @@ class FilesSyncWork( needsWifi = syncedFolder.isWifiOnly uploadAction = syncedFolder.uploadAction } + FileUploader.uploadNewFile( - context, user, localPaths, remotePaths, - mimetypes, uploadAction!!, true, // create parent folder if not existent UploadFileOperation.CREATED_AS_INSTANT_PICTURE, diff --git a/app/src/main/java/com/nextcloud/client/jobs/FilesUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/FilesUploadWorker.kt index 8ddc420e4d12..ce3516740a6f 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/FilesUploadWorker.kt +++ b/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, accountName: String) { diff --git a/app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt b/app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt index ba2945dc71f3..bd59f07fe6e0 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt +++ b/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? { diff --git a/app/src/main/java/com/nextcloud/client/jobs/JobInfo.kt b/app/src/main/java/com/nextcloud/client/jobs/JobInfo.kt index 4a1de45f5644..95379fcbf023 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/JobInfo.kt +++ b/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 +) diff --git a/app/src/main/java/com/nextcloud/client/jobs/JobsModule.kt b/app/src/main/java/com/nextcloud/client/jobs/JobsModule.kt index 6be8c5d5058d..79f2f2386c3d 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/JobsModule.kt +++ b/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) } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/TestJob.kt b/app/src/main/java/com/nextcloud/client/jobs/TestJob.kt index 0cd0684cd44a..cfd9e996c0a3 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/TestJob.kt +++ b/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 } } diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java index 57314e9a9c5f..bb93d3fa5ec8 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java +++ b/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 @@ default void onDarkThemeModeChanged(DarkMode mode) { */ int getLastSeenVersionCode(); + void saveLogEntry(List logEntryList); + + List readLogEntry(); + + + /** * Saves the version code as the last seen version code. * diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java index 3e0ea437fbfa..4643f18dbc82 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java @@ -28,11 +28,13 @@ 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.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.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 int getLastSeenVersionCode() { return preferences.getInt(AUTO_PREF__LAST_SEEN_VERSION_CODE, 0); } + @Override + public void saveLogEntry(List logEntryList) { + Gson gson = new Gson(); + String json = gson.toJson(logEntryList); + preferences.edit().putString(LOG_ENTRY, json).apply(); + } + + @Override + public List readLogEntry() { + String json = preferences.getString(LOG_ENTRY, null); + if (json == null) return emptyList(); + Gson gson = new Gson(); + Type listType = new TypeToken>() {}.getType(); + return gson.fromJson(json, listType); + } + @Override public void setLastSeenVersionCode(int versionCode) { preferences.edit().putInt(AUTO_PREF__LAST_SEEN_VERSION_CODE, versionCode).apply(); diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt new file mode 100644 index 000000000000..a4d23689491f --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt @@ -0,0 +1,46 @@ +/* + * Nextcloud Android client application + * + * @author Alper Ozturk + * Copyright (C) 2023 Alper Ozturk + * 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 . + */ + +package com.nextcloud.utils.extensions + +import android.content.Context +import android.graphics.Outline +import android.util.TypedValue +import android.view.View +import android.view.ViewOutlineProvider + +fun createRoundedOutline(context: Context, cornerRadiusValue: Float): ViewOutlineProvider { + return object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + val left = 0 + val top = 0 + val right = view.width + val bottom = view.height + val cornerRadius = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + cornerRadiusValue, + context.resources.displayMetrics + ).toInt() + + outline.setRoundRect(left, top, right, bottom, cornerRadius.toFloat()) + } + } +} diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index 4911950ed51a..63f23b3f5184 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -129,7 +129,6 @@ * Contains methods to build the "static" strings. These strings were before constants in different classes */ public class MainApp extends MultiDexApplication implements HasAndroidInjector { - public static final OwnCloudVersion OUTDATED_SERVER_VERSION = NextcloudVersion.nextcloud_23; public static final OwnCloudVersion MINIMUM_SUPPORTED_SERVER_VERSION = OwnCloudVersion.nextcloud_16; diff --git a/app/src/main/java/com/owncloud/android/files/services/FileUploader.java b/app/src/main/java/com/owncloud/android/files/services/FileUploader.java index 040a844c0691..b49d24093f63 100644 --- a/app/src/main/java/com/owncloud/android/files/services/FileUploader.java +++ b/app/src/main/java/com/owncloud/android/files/services/FileUploader.java @@ -79,6 +79,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.files.FileUtils; +import com.owncloud.android.lib.resources.files.model.ServerFileInterface; import com.owncloud.android.operations.UploadFileOperation; import com.owncloud.android.ui.activity.ConflictsResolveActivity; import com.owncloud.android.ui.activity.UploadListActivity; @@ -902,12 +903,10 @@ private void cancelPendingUploads(String accountName) { * Upload a new file */ public static void uploadNewFile( - Context context, User user, String localPath, String remotePath, int behaviour, - String mimeType, boolean createRemoteFile, int createdBy, boolean requiresWifi, @@ -915,11 +914,9 @@ public static void uploadNewFile( NameCollisionPolicy nameCollisionPolicy ) { uploadNewFile( - context, user, new String[]{localPath}, new String[]{remotePath}, - new String[]{mimeType}, behaviour, createRemoteFile, createdBy, @@ -933,11 +930,9 @@ public static void uploadNewFile( * Upload multiple new files */ public static void uploadNewFile( - Context context, User user, String[] localPaths, String[] remotePaths, - String[] mimeTypes, Integer behaviour, Boolean createRemoteFolder, int createdBy, @@ -945,39 +940,15 @@ public static void uploadNewFile( boolean requiresCharging, NameCollisionPolicy nameCollisionPolicy ) { - - - if (useFilesUploadWorker(context)) { - new FilesUploadHelper().uploadNewFiles(user, - localPaths, - remotePaths, - createRemoteFolder, - createdBy, - requiresWifi, - requiresCharging, - nameCollisionPolicy, - behaviour); - } else { - Intent intent = new Intent(context, FileUploader.class); - - intent.putExtra(FileUploader.KEY_ACCOUNT, user.toPlatformAccount()); - intent.putExtra(FileUploader.KEY_USER, user); - intent.putExtra(FileUploader.KEY_LOCAL_FILE, localPaths); - intent.putExtra(FileUploader.KEY_REMOTE_FILE, remotePaths); - intent.putExtra(FileUploader.KEY_MIME_TYPE, mimeTypes); - intent.putExtra(FileUploader.KEY_LOCAL_BEHAVIOUR, behaviour); - intent.putExtra(FileUploader.KEY_CREATE_REMOTE_FOLDER, createRemoteFolder); - intent.putExtra(FileUploader.KEY_CREATED_BY, createdBy); - intent.putExtra(FileUploader.KEY_WHILE_ON_WIFI_ONLY, requiresWifi); - intent.putExtra(FileUploader.KEY_WHILE_CHARGING_ONLY, requiresCharging); - intent.putExtra(FileUploader.KEY_NAME_COLLISION_POLICY, nameCollisionPolicy); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(intent); - } else { - context.startService(intent); - } - } + new FilesUploadHelper().uploadNewFiles(user, + localPaths, + remotePaths, + createRemoteFolder, + createdBy, + requiresWifi, + requiresCharging, + nameCollisionPolicy, + behaviour); } /** @@ -1006,8 +977,7 @@ public static void uploadUpdateFile( OCFile existingFile, Integer behaviour, NameCollisionPolicy nameCollisionPolicy, - boolean disableRetries - ) { + boolean disableRetries) { uploadUpdateFile(context, user, new OCFile[]{existingFile}, @@ -1036,13 +1006,7 @@ public static void uploadUpdateFile( intent.putExtra(FileUploader.KEY_NAME_COLLISION_POLICY, nameCollisionPolicy); intent.putExtra(FileUploader.KEY_DISABLE_RETRIES, disableRetries); - if (useFilesUploadWorker(context)) { - new FilesUploadHelper().uploadUpdatedFile(user, existingFiles, behaviour, nameCollisionPolicy); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(intent); - } else { - context.startService(intent); - } + new FilesUploadHelper().uploadUpdatedFile(user, existingFiles, behaviour, nameCollisionPolicy); } /** @@ -1057,13 +1021,7 @@ public static void retryUpload(@NonNull Context context, i.putExtra(FileUploader.KEY_ACCOUNT, user.toPlatformAccount()); i.putExtra(FileUploader.KEY_RETRY_UPLOAD, upload); - if (useFilesUploadWorker(context)) { - new FilesUploadHelper().retryUpload(upload, user); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(i); - } else { - context.startService(i); - } + new FilesUploadHelper().retryUpload(upload, user); } /** @@ -1129,16 +1087,6 @@ public static String getUploadFinishMessage() { return FileUploader.class.getName() + UPLOAD_FINISH_MESSAGE; } - - private static boolean useFilesUploadWorker(Context context) { - if (forceNewUploadWorker) { - return true; - } - - // bump min version down with every release until minSDK is reached, at that point get rid of old upload code - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || context.getResources().getBoolean(R.bool.is_beta); - } - @VisibleForTesting public static void setForceNewUploadWorker(final Boolean value) { forceNewUploadWorker = value; @@ -1150,7 +1098,6 @@ public static void setForceNewUploadWorker(final Boolean value) { * It provides by itself the available operations. */ public class FileUploaderBinder extends Binder implements OnDatatransferProgressListener { - /** * Map of listeners that will be reported about progress of uploads from a {@link FileUploaderBinder} instance */ @@ -1162,7 +1109,7 @@ public class FileUploaderBinder extends Binder implements OnDatatransferProgress * @param account ownCloud account where the remote file will be stored. * @param file A file in the queue of pending uploads */ - public void cancel(Account account, OCFile file) { + public void cancel(Account account, ServerFileInterface file) { cancel(account.name, file.getRemotePath(), null); } @@ -1183,34 +1130,10 @@ public void cancel(OCUpload storedUpload) { * @param resultCode Setting result code will pause rather than cancel the job */ public void cancel(String accountName, String remotePath, @Nullable ResultCode resultCode) { - // Cancel for Android version >= Android 11 - if (useFilesUploadWorker(getApplicationContext())) { - try { - new FilesUploadHelper().cancelFileUpload(remotePath, accountManager.getUser(accountName).get()); - } catch (NoSuchElementException e) { - Log_OC.e(TAG, "Error cancelling current upload because user does not exist!"); - } - } else { - // Cancel for Android version <= Android 10 - Pair removeResult = mPendingUploads.remove(accountName, remotePath); - UploadFileOperation upload = removeResult.first; - if (upload == null && mCurrentUpload != null && mCurrentAccount != null && - mCurrentUpload.getRemotePath().startsWith(remotePath) && accountName.equals(mCurrentAccount.name)) { - - upload = mCurrentUpload; - } - - if (upload != null) { - upload.cancel(resultCode); - // need to update now table in mUploadsStorageManager, - // since the operation will not get to be run by FileUploader#uploadFile - if (resultCode != null) { - mUploadsStorageManager.updateDatabaseUploadResult(new RemoteOperationResult(resultCode), upload); - notifyUploadResult(upload, new RemoteOperationResult(resultCode)); - } else { - mUploadsStorageManager.removeUpload(accountName, remotePath); - } - } + try { + new FilesUploadHelper().cancelFileUpload(remotePath, accountManager.getUser(accountName).get()); + } catch (NoSuchElementException e) { + Log_OC.e(TAG, "Error cancelling current upload because user does not exist!"); } } @@ -1225,14 +1148,7 @@ public void cancel(User user) { public void cancel(String accountName) { cancelPendingUploads(accountName); - if (useFilesUploadWorker(getApplicationContext())) { - new FilesUploadHelper().restartUploadJob(accountManager.getUser(accountName).get()); - } else { - if (mCurrentUpload != null && mCurrentUpload.getUser().nameEquals(accountName)) { - mCurrentUpload.cancel(ResultCode.CANCELLED); - } - } - + new FilesUploadHelper().restartUploadJob(accountManager.getUser(accountName).get()); } public void clearListeners() { @@ -1256,43 +1172,27 @@ public boolean isUploading(User user, OCFile file) { if (user == null || file == null) { return false; } - if (useFilesUploadWorker(getApplicationContext())){ - // Not same as for service because upload list is "created" on the spot in the worker and not available here - OCUpload upload = mUploadsStorageManager.getUploadByRemotePath(file.getRemotePath()); - if (upload == null){ - return false; - } - return upload.getUploadStatus() == UploadStatus.UPLOAD_IN_PROGRESS; + OCUpload upload = mUploadsStorageManager.getUploadByRemotePath(file.getRemotePath()); - }else{ - return mPendingUploads.contains(user.getAccountName(), file.getRemotePath()); + if (upload == null){ + return false; } + + return upload.getUploadStatus() == UploadStatus.UPLOAD_IN_PROGRESS; } @SuppressFBWarnings("NP") public boolean isUploadingNow(OCUpload upload) { - if (useFilesUploadWorker(getApplicationContext())){ - UploadFileOperation currentUploadFileOperation = FilesUploadWorker.Companion.getCurrentUploadFileOperation(); - if (currentUploadFileOperation == null || currentUploadFileOperation.getUser() == null) return false; - if (upload == null || (!upload.getAccountName().equals(currentUploadFileOperation.getUser().getAccountName()))) return false; - if (currentUploadFileOperation.getOldFile() != null){ - // For file conflicts check old file remote path - return upload.getRemotePath().equals(currentUploadFileOperation.getRemotePath()) || - upload.getRemotePath().equals(currentUploadFileOperation.getOldFile().getRemotePath()); - } - return upload.getRemotePath().equals(currentUploadFileOperation.getRemotePath()); - - }else { - - return upload != null && - mCurrentAccount != null && - mCurrentUpload != null && - upload.getAccountName().equals(mCurrentAccount.name) && - (upload.getRemotePath().equals(mCurrentUpload.getRemotePath()) || - (mCurrentUpload.getOldFile() != null && - upload.getRemotePath().equals(mCurrentUpload.getOldFile().getRemotePath()))); + UploadFileOperation currentUploadFileOperation = FilesUploadWorker.Companion.getCurrentUploadFileOperation(); + if (currentUploadFileOperation == null || currentUploadFileOperation.getUser() == null) return false; + if (upload == null || (!upload.getAccountName().equals(currentUploadFileOperation.getUser().getAccountName()))) return false; + if (currentUploadFileOperation.getOldFile() != null){ + // For file conflicts check old file remote path + return upload.getRemotePath().equals(currentUploadFileOperation.getRemotePath()) || + upload.getRemotePath().equals(currentUploadFileOperation.getOldFile().getRemotePath()); } + return upload.getRemotePath().equals(currentUploadFileOperation.getRemotePath()); } /** @@ -1305,18 +1205,14 @@ public boolean isUploadingNow(OCUpload upload) { public void addDatatransferProgressListener( OnDatatransferProgressListener listener, User user, - OCFile file + ServerFileInterface file ) { if (user == null || file == null || listener == null) { return; } - String targetKey = buildRemoteName(user.getAccountName(), file.getRemotePath()); - if (useFilesUploadWorker(getApplicationContext())) { - new FilesUploadHelper().addDatatransferProgressListener(listener,targetKey); - }else { - mBoundListeners.put(targetKey, listener); - } + String targetKey = buildRemoteName(user.getAccountName(), file.getRemotePath()); + new FilesUploadHelper().addDatatransferProgressListener(listener,targetKey); } /** @@ -1334,11 +1230,7 @@ public void addDatatransferProgressListener( } String targetKey = buildRemoteName(ocUpload.getAccountName(), ocUpload.getRemotePath()); - if (useFilesUploadWorker(getApplicationContext())) { - new FilesUploadHelper().addDatatransferProgressListener(listener,targetKey); - }else { - mBoundListeners.put(targetKey, listener); - } + new FilesUploadHelper().addDatatransferProgressListener(listener,targetKey); } /** @@ -1351,21 +1243,14 @@ public void addDatatransferProgressListener( public void removeDatatransferProgressListener( OnDatatransferProgressListener listener, User user, - OCFile file + ServerFileInterface file ) { if (user == null || file == null || listener == null) { return; } String targetKey = buildRemoteName(user.getAccountName(), file.getRemotePath()); - - if (useFilesUploadWorker(getApplicationContext())) { - new FilesUploadHelper().removeDatatransferProgressListener(listener,targetKey); - }else { - if (mBoundListeners.get(targetKey) == listener) { - mBoundListeners.remove(targetKey); - } - } + new FilesUploadHelper().removeDatatransferProgressListener(listener,targetKey); } /** @@ -1383,14 +1268,7 @@ public void removeDatatransferProgressListener( } String targetKey = buildRemoteName(ocUpload.getAccountName(), ocUpload.getRemotePath()); - - if (useFilesUploadWorker(getApplicationContext())) { - new FilesUploadHelper().removeDatatransferProgressListener(listener,targetKey); - }else { - if (mBoundListeners.get(targetKey) == listener) { - mBoundListeners.remove(targetKey); - } - } + new FilesUploadHelper().removeDatatransferProgressListener(listener,targetKey); } @Override diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java index 6ca6aaa6f062..dad544bc176c 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -906,8 +906,15 @@ private void requestUploadOfFilesFromFileSystem(String localBasePath, String[] f default -> FileUploader.LOCAL_BEHAVIOUR_FORGET; }; - FileUploader.uploadNewFile(this, getUser().orElseThrow(RuntimeException::new), filePaths, remotePaths, null, // MIME type will be detected from file name - behaviour, true, UploadFileOperation.CREATED_BY_USER, false, false, NameCollisionPolicy.ASK_USER); + FileUploader.uploadNewFile(getUser().orElseThrow(RuntimeException::new), + filePaths, + remotePaths, + behaviour, + true, + UploadFileOperation.CREATED_BY_USER, + false, + false, + NameCollisionPolicy.ASK_USER); } else { Log_OC.d(TAG, "User clicked on 'Update' with no selection"); @@ -1471,6 +1478,7 @@ public void browseToRoot() { setFile(listOfFiles.getCurrentFile()); startSyncFolderOperation(root, false); } + binding.fabMain.setImageResource(R.drawable.ic_plus); resetTitleBarAndScrolling(); } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java index fe22c560c204..29dc339c5401 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java @@ -885,12 +885,10 @@ private boolean somethingToUpload() { public void uploadFile(String tmpName, String filename) { FileUploader.uploadNewFile( - getBaseContext(), getUser().orElseThrow(RuntimeException::new), tmpName, mFile.getRemotePath() + filename, FileUploader.LOCAL_BEHAVIOUR_COPY, - null, true, UploadFileOperation.CREATED_BY_USER, false, diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java index d4f3d5c95094..8d5e5ac5293c 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java @@ -196,19 +196,22 @@ private void loadItems() { private void refresh() { backgroundJobManager.startImmediateFilesSyncJob(false, true); - // retry failed uploads - new Thread(() -> FileUploader.retryFailedUploads( - this, - uploadsStorageManager, - connectivityService, - userAccountManager, - powerManagementService)) - .start(); + if(uploadsStorageManager.getFailedUploads().length > 0){ + // retry failed uploads + new Thread(() -> FileUploader.retryFailedUploads( + this, + uploadsStorageManager, + connectivityService, + userAccountManager, + powerManagementService)) + .start(); + DisplayUtils.showSnackMessage(this, R.string.uploader_local_files_uploaded); + } + // update UI uploadListAdapter.loadUploadItemsFromDb(); swipeListRefreshLayout.setRefreshing(false); - DisplayUtils.showSnackMessage(this, R.string.uploader_local_files_uploaded); } @Override diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt index 3d40631fb24d..298657a8b659 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt @@ -22,7 +22,9 @@ package com.owncloud.android.ui.adapter import android.view.View +import android.widget.ImageButton import android.widget.ImageView +import android.widget.LinearLayout import android.widget.TextView import com.elyeproj.loaderviewlibrary.LoaderImageView @@ -32,12 +34,14 @@ interface ListGridImageViewHolder { val shimmerThumbnail: LoaderImageView val favorite: ImageView val localFileIndicator: ImageView + val imageFileName: TextView? val shared: ImageView val checkbox: ImageView val itemLayout: View val unreadComments: ImageView - - val gridLivePhotoIndicator: TextView? + val more: ImageButton? + val fileFeaturesLayout: LinearLayout? + val gridLivePhotoIndicator: ImageView? val livePhotoIndicator: TextView? val livePhotoIndicatorSeparator: TextView? } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java index 1cc501642a5b..20f8842487cb 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java @@ -32,6 +32,7 @@ import android.widget.LinearLayout; import android.widget.TextView; +import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.client.preferences.AppPreferences; import com.owncloud.android.R; import com.owncloud.android.datamodel.ThumbnailsCacheManager; @@ -54,6 +55,7 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; /** @@ -186,11 +188,10 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi } else { gridViewHolder.checkbox.setVisibility(View.VISIBLE); if (isCheckedFile(file)) { - gridViewHolder.itemLayout.setBackgroundColor(mContext.getResources() - .getColor(R.color.selected_item_background)); + gridViewHolder.itemLayout.setBackgroundColor(ContextCompat.getColor(mContext, R.color.selected_item_background)); gridViewHolder.checkbox.setImageDrawable( - viewThemeUtils.platform.tintPrimaryDrawable(mContext, R.drawable.ic_checkbox_marked)); + viewThemeUtils.platform.tintDrawable(mContext, R.drawable.ic_checkbox_marked, ColorRole.PRIMARY)); } else { gridViewHolder.itemLayout.setBackgroundColor(mContext.getResources().getColor(R.color.bg_default)); gridViewHolder.checkbox.setImageResource(R.drawable.ic_checkbox_blank_outline); diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 00c9868ed4c6..32103f0e7f0f 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -39,7 +39,9 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageButton; import android.widget.ImageView; +import android.widget.LinearLayout; import com.elyeproj.loaderviewlibrary.LoaderImageView; import com.nextcloud.android.common.ui.theme.utils.ColorRole; @@ -300,6 +302,7 @@ public long getItemId(int position) { return headerId; } + // skip header position--; } @@ -365,8 +368,7 @@ public boolean isEmpty() { @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { - default: - case VIEWTYPE_ITEM: + default -> { if (gridView) { return new OCFileListGridItemViewHolder( GridItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) @@ -376,8 +378,8 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int ListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) ); } - - case VIEWTYPE_IMAGE: + } + case VIEWTYPE_IMAGE -> { if (gridView) { return new OCFileListGridImageViewHolder( GridImageBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) @@ -387,23 +389,22 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int ListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) ); } - - case VIEWTYPE_FOOTER: + } + case VIEWTYPE_FOOTER -> { return new OCFileListFooterViewHolder( ListFooterBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) ); - - case VIEWTYPE_HEADER: + } + case VIEWTYPE_HEADER -> { ListHeaderBinding binding = ListHeaderBinding.inflate( LayoutInflater.from(parent.getContext()), parent, false); - ViewGroup.LayoutParams layoutParams = binding.headerView.getLayoutParams(); layoutParams.height = (int) (parent.getHeight() * 0.3); binding.headerView.setLayoutParams(layoutParams); - return new OCFileListHeaderViewHolder(binding); + } } } @@ -430,6 +431,8 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi } ocFileListDelegate.bindGridViewHolder(gridViewHolder, file, searchType); + checkVisibilityOfMoreButtons(gridViewHolder); + checkVisibilityOfFileFeaturesLayout(gridViewHolder); if (holder instanceof ListItemViewHolder) { bindListItemViewHolder((ListItemViewHolder) gridViewHolder, file); @@ -437,12 +440,45 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi if (holder instanceof ListGridItemViewHolder) { bindListGridItemViewHolder((ListGridItemViewHolder) holder, file); + checkVisibilityOfMoreButtons((ListGridItemViewHolder) holder); + checkVisibilityOfFileFeaturesLayout((ListGridItemViewHolder) holder); } updateLivePhotoIndicators(gridViewHolder, file); } } + private void checkVisibilityOfFileFeaturesLayout(ListGridImageViewHolder holder) { + int fileFeaturesVisibility = View.GONE; + LinearLayout fileFeaturesLayout = holder.getFileFeaturesLayout(); + + if (fileFeaturesLayout == null) { + return; + } + + for (int i = 0; i < fileFeaturesLayout.getChildCount(); i++) { + View child = fileFeaturesLayout.getChildAt(i); + if (child.getVisibility() == View.VISIBLE) { + fileFeaturesVisibility = View.VISIBLE; + } + } + + fileFeaturesLayout.setVisibility(fileFeaturesVisibility); + } + + private void checkVisibilityOfMoreButtons(ListGridImageViewHolder holder) { + ImageButton moreButton = holder.getMore(); + if (moreButton == null) { + return; + } + + if (isMultiSelect()) { + moreButton.setVisibility(View.GONE); + } else { + moreButton.setVisibility(View.VISIBLE); + } + } + private void mergeOCFilesForLivePhoto() { List filesToRemove = new ArrayList<>(); diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt index b6b7abe2cba9..e350d8b7c856 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt @@ -27,10 +27,13 @@ import android.graphics.drawable.ColorDrawable import android.os.AsyncTask import android.view.View import android.widget.ImageView +import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import com.elyeproj.loaderviewlibrary.LoaderImageView +import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.utils.extensions.createRoundedOutline import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile @@ -142,7 +145,7 @@ class OCFileListDelegate( storageManager, asyncGalleryTasks, file.remoteId, - context.resources.getColor(R.color.bg_default) + ContextCompat.getColor(context, R.color.bg_default) ) var drawable = MimeTypeUtil.getFileTypeIcon( file.mimeType, @@ -204,6 +207,7 @@ class OCFileListDelegate( searchType: SearchType? ) { // thumbnail + gridViewHolder.imageFileName?.text = file.fileName gridViewHolder.thumbnail.tag = file.fileId DisplayUtils.setThumbnail( file, @@ -218,6 +222,7 @@ class OCFileListDelegate( viewThemeUtils, syncFolderProvider ) + // item layout + click listeners bindGridItemLayout(file, gridViewHolder) @@ -232,17 +237,20 @@ class OCFileListDelegate( } // download state - gridViewHolder.localFileIndicator.visibility = View.INVISIBLE // default first + gridViewHolder.localFileIndicator.visibility = View.GONE // default first // metadata (downloaded, favorite) bindGridMetadataViews(file, gridViewHolder) // shares - val shouldHideShare = gridView || + val shouldHideShare = ( hideItemOptions || - !file.isFolder && file.isEncrypted || - file.isEncrypted && !EncryptionUtils.supportsSecureFiledrop(file, user) || - searchType == SearchType.FAVORITE_SEARCH + !file.isFolder && + file.isEncrypted || + file.isEncrypted && + !EncryptionUtils.supportsSecureFiledrop(file, user) || + searchType == SearchType.FAVORITE_SEARCH + ) if (shouldHideShare) { gridViewHolder.shared.visibility = View.GONE } else { @@ -263,34 +271,63 @@ class OCFileListDelegate( } private fun bindGridItemLayout(file: OCFile, gridViewHolder: ListGridImageViewHolder) { - if (highlightedItem != null && file.fileId == highlightedItem!!.fileId) { - gridViewHolder.itemLayout.setBackgroundColor( - context.resources - .getColor(R.color.selected_item_background) - ) - } else if (isCheckedFile(file)) { - gridViewHolder.itemLayout.setBackgroundColor( - context.resources - .getColor(R.color.selected_item_background) - ) - gridViewHolder.checkbox.setImageDrawable( - viewThemeUtils.platform.tintPrimaryDrawable(context, R.drawable.ic_checkbox_marked) - ) - } else { - gridViewHolder.itemLayout.setBackgroundColor(context.resources.getColor(R.color.bg_default)) - gridViewHolder.checkbox.setImageResource(R.drawable.ic_checkbox_blank_outline) + setItemLayoutBackgroundColor(file, gridViewHolder) + setCheckBoxImage(file, gridViewHolder) + setItemLayoutOnClickListeners(file, gridViewHolder) + + gridViewHolder.more?.setOnClickListener { + ocFileListFragmentInterface.onOverflowIconClicked(file, it) } + } + + private fun setItemLayoutOnClickListeners(file: OCFile, gridViewHolder: ListGridImageViewHolder) { gridViewHolder.itemLayout.setOnClickListener { ocFileListFragmentInterface.onItemClicked(file) } + if (!hideItemOptions) { - gridViewHolder.itemLayout.isLongClickable = true - gridViewHolder.itemLayout.setOnLongClickListener { - ocFileListFragmentInterface.onLongItemClicked( - file - ) + gridViewHolder.itemLayout.apply { + isLongClickable = true + setOnLongClickListener { + ocFileListFragmentInterface.onLongItemClicked( + file + ) + } } } } + private fun setItemLayoutBackgroundColor(file: OCFile, gridViewHolder: ListGridImageViewHolder) { + val cornerRadius = context.resources.getDimension(R.dimen.selected_grid_container_radius) + + val isDarkModeActive = (syncFolderProvider?.preferences?.isDarkModeEnabled == true) + val selectedItemBackgroundColorId: Int = if (isDarkModeActive) { + R.color.action_mode_background + } else { + R.color.selected_item_background + } + + val itemLayoutBackgroundColorId: Int = if (file.fileId == highlightedItem?.fileId || isCheckedFile(file)) { + selectedItemBackgroundColorId + } else { + R.color.bg_default + } + + gridViewHolder.itemLayout.apply { + outlineProvider = createRoundedOutline(context, cornerRadius) + clipToOutline = true + setBackgroundColor(ContextCompat.getColor(context, itemLayoutBackgroundColorId)) + } + } + + private fun setCheckBoxImage(file: OCFile, gridViewHolder: ListGridImageViewHolder) { + if (isCheckedFile(file)) { + gridViewHolder.checkbox.setImageDrawable( + viewThemeUtils.platform.tintDrawable(context, R.drawable.ic_checkbox_marked, ColorRole.PRIMARY) + ) + } else { + gridViewHolder.checkbox.setImageResource(R.drawable.ic_checkbox_blank_outline) + } + } + private fun bindGridMetadataViews(file: OCFile, gridViewHolder: ListGridImageViewHolder) { if (showMetadata) { showLocalFileIndicator(file, gridViewHolder) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt index f5e7fd01ff5a..95d0417b2a74 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt @@ -22,7 +22,9 @@ package com.owncloud.android.ui.adapter import android.view.View +import android.widget.ImageButton import android.widget.ImageView +import android.widget.LinearLayout import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.elyeproj.loaderviewlibrary.LoaderImageView @@ -33,9 +35,13 @@ internal class OCFileListGridImageViewHolder(var binding: GridImageBinding) : binding.root ), ListGridImageViewHolder { + override val thumbnail: ImageView get() = binding.thumbnail + override val imageFileName: TextView + get() = binding.Filename + override fun showVideoOverlay() { // noop } @@ -54,8 +60,11 @@ internal class OCFileListGridImageViewHolder(var binding: GridImageBinding) : get() = binding.ListItemLayout override val unreadComments: ImageView get() = binding.unreadComments - - override val gridLivePhotoIndicator: TextView + override val more: ImageButton + get() = binding.more + override val fileFeaturesLayout: LinearLayout + get() = binding.fileFeaturesLayout + override val gridLivePhotoIndicator: ImageView get() = binding.gridLivePhotoIndicator override val livePhotoIndicator: TextView? get() = null diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt index 3f2978cdb0ee..31967fe8ce17 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt @@ -22,7 +22,9 @@ package com.owncloud.android.ui.adapter import android.view.View +import android.widget.ImageButton import android.widget.ImageView +import android.widget.LinearLayout import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.elyeproj.loaderviewlibrary.LoaderImageView @@ -48,6 +50,8 @@ internal class OCFileListGridItemViewHolder(var binding: GridItemBinding) : get() = binding.favoriteAction override val localFileIndicator: ImageView get() = binding.localFileIndicator + override val imageFileName: TextView? + get() = null override val shared: ImageView get() = binding.sharedIcon override val checkbox: ImageView @@ -57,12 +61,16 @@ internal class OCFileListGridItemViewHolder(var binding: GridItemBinding) : override val unreadComments: ImageView get() = binding.unreadComments - override val gridLivePhotoIndicator: TextView? + override val gridLivePhotoIndicator: ImageView? get() = null override val livePhotoIndicator: TextView? get() = null override val livePhotoIndicatorSeparator: TextView? get() = null + override val fileFeaturesLayout: LinearLayout + get() = binding.fileFeaturesLayout + override val more: ImageButton + get() = binding.more init { binding.favoriteAction.drawable.mutate() diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt index 51ae45c13958..2c62a795e2e9 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt @@ -22,6 +22,7 @@ package com.owncloud.android.ui.adapter import android.view.View +import android.widget.ImageButton import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView @@ -37,7 +38,7 @@ internal class OCFileListItemViewHolder(private var binding: ListItemBinding) : binding.root ), ListItemViewHolder { - override val gridLivePhotoIndicator: TextView? + override val gridLivePhotoIndicator: ImageView? get() = null override val livePhotoIndicator: TextView get() = binding.livePhotoIndicator @@ -73,12 +74,18 @@ internal class OCFileListItemViewHolder(private var binding: ListItemBinding) : binding.thumbnailLayout.videoOverlay.visibility = View.VISIBLE } + override val more: ImageButton? + get() = null + override val fileFeaturesLayout: LinearLayout? + get() = null override val shimmerThumbnail: LoaderImageView get() = binding.thumbnailLayout.thumbnailShimmer override val favorite: ImageView get() = binding.favoriteAction override val localFileIndicator: ImageView get() = binding.localFileIndicator + override val imageFileName: TextView? + get() = null override val shared: ImageView get() = binding.sharedIcon override val checkbox: ImageView diff --git a/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java b/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java index 63efab826094..20af7dc2ecfc 100644 --- a/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java +++ b/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java @@ -192,8 +192,7 @@ protected ResultCode doInBackground(Object[] params) { user, fullTempPath, currentRemotePath, - behaviour, - leakedContentResolver.getType(currentUri) + behaviour ); fullTempPath = null; } @@ -247,14 +246,12 @@ protected ResultCode doInBackground(Object[] params) { return result; } - private void requestUpload(User user, String localPath, String remotePath, int behaviour, String mimeType) { + private void requestUpload(User user, String localPath, String remotePath, int behaviour) { FileUploader.uploadNewFile( - mAppContext, user, localPath, remotePath, behaviour, - mimeType, false, // do not create parent folder if not existent UploadFileOperation.CREATED_BY_USER, false, diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/UriUploader.kt b/app/src/main/java/com/owncloud/android/ui/helpers/UriUploader.kt index 69aec9b24af5..263530fc8dd6 100644 --- a/app/src/main/java/com/owncloud/android/ui/helpers/UriUploader.kt +++ b/app/src/main/java/com/owncloud/android/ui/helpers/UriUploader.kt @@ -126,12 +126,10 @@ class UriUploader( */ private fun requestUpload(localPath: String?, remotePath: String) { FileUploader.uploadNewFile( - mActivity, user, localPath, remotePath, mBehaviour, - null, // MIME type will be detected from file name false, // do not create parent folder if not existent UploadFileOperation.CREATED_BY_USER, false, diff --git a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java index 3437556268d4..c5618bdce2df 100644 --- a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -977,9 +977,9 @@ public static void stopShimmer(@Nullable LoaderImageView thumbnailShimmer, Image } private static void configShimmerGridImageSize(LoaderImageView thumbnailShimmer, float gridColumns) { - FrameLayout.LayoutParams targetLayoutParams = (FrameLayout.LayoutParams) thumbnailShimmer.getLayoutParams(); - try { + FrameLayout.LayoutParams targetLayoutParams = (FrameLayout.LayoutParams) thumbnailShimmer.getLayoutParams(); + final Point screenSize = getScreenSize(thumbnailShimmer.getContext()); final int marginLeftAndRight = targetLayoutParams.leftMargin + targetLayoutParams.rightMargin; final int size = Math.round(screenSize.x / gridColumns - marginLeftAndRight); diff --git a/app/src/main/java/com/owncloud/android/utils/FilesUploadHelper.kt b/app/src/main/java/com/owncloud/android/utils/FilesUploadHelper.kt index b15c4910bdfc..0b146c36a11a 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesUploadHelper.kt +++ b/app/src/main/java/com/owncloud/android/utils/FilesUploadHelper.kt @@ -151,7 +151,6 @@ class FilesUploadHelper { val boundListener = mBoundListeners[key] boundListener?.onTransferProgress(progressRate, totalTransferredSoFar, totalToTransfer, fileName) - Log_OC.d("TAG", "Hello") } } } diff --git a/app/src/main/res/layout/etm_background_job_list_item.xml b/app/src/main/res/layout/etm_background_job_list_item.xml index 1cd78552ebb0..1801d2f7bd22 100644 --- a/app/src/main/res/layout/etm_background_job_list_item.xml +++ b/app/src/main/res/layout/etm_background_job_list_item.xml @@ -136,4 +136,48 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/grid_image.xml b/app/src/main/res/layout/grid_image.xml index 654d3dcee63a..eb6437fb1848 100644 --- a/app/src/main/res/layout/grid_image.xml +++ b/app/src/main/res/layout/grid_image.xml @@ -15,129 +15,176 @@ along with this program. If not, see . --> - - - - - - - - - - - - - + android:layout_height="@dimen/grid_container_height"> - + + + + + + - - - - - - - - - + android:gravity="center" + android:layout_marginEnd="@dimen/grid_layout_file_features_margin_end" + android:layout_marginBottom="@dimen/grid_layout_margin_bottom" + android:alpha="0.9" + android:background="@drawable/rounded_rect" + android:backgroundTint="@color/grid_file_features_background_color" + android:orientation="horizontal" + android:padding="@dimen/standard_quarter_padding" + android:translationZ="4dp" + app:layout_constraintBottom_toBottomOf="@+id/thumbnail" + app:layout_constraintEnd_toEndOf="parent"> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/grid_item.xml b/app/src/main/res/layout/grid_item.xml index f6ab35c6d2d8..c3a4a4136675 100644 --- a/app/src/main/res/layout/grid_item.xml +++ b/app/src/main/res/layout/grid_item.xml @@ -15,130 +15,164 @@ along with this program. If not, see . --> - + android:layout_width="match_parent" + android:layout_height="@dimen/grid_container_height"> + android:layout_width="@dimen/grid_container_width" + android:layout_height="@dimen/grid_container_height"> + + + + - - - - - - - - - - - + android:gravity="center" + android:layout_marginEnd="@dimen/grid_layout_file_features_margin_end" + android:layout_marginBottom="@dimen/grid_layout_margin_bottom" + android:alpha="0.9" + android:background="@drawable/rounded_rect" + android:backgroundTint="@color/grid_file_features_background_color" + android:orientation="horizontal" + android:padding="@dimen/standard_quarter_padding" + android:translationZ="4dp" + app:layout_constraintBottom_toBottomOf="@+id/thumbnail" + app:layout_constraintEnd_toEndOf="parent"> + + + + android:src="@drawable/shared_via_link" + app:tint="@color/grid_file_features_icon_color" + tools:visibility="visible" /> + android:visibility="gone" + app:tint="@color/grid_file_features_icon_color" + tools:visibility="visible" /> + android:src="@drawable/ic_synced" + tools:visibility="visible" /> + + + + + + + + - - - - - - + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index d72d9d458f83..58fcdecf1fc2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -23,6 +23,8 @@ #ffffff #B3FFFFFF #333333 + #303034 + #E9E8EB @color/secondary_text_color #ffffff #ff888888 diff --git a/app/src/main/res/values/dims.xml b/app/src/main/res/values/dims.xml index da4922dad63a..21f86373c9f2 100644 --- a/app/src/main/res/values/dims.xml +++ b/app/src/main/res/values/dims.xml @@ -40,6 +40,11 @@ 32dp 8dp 4dp + 10dp + 24dp + 2dp + 16dp + 22sp 140dp 180dp 2dp @@ -103,6 +108,17 @@ 48dp 24dp 24dp + 4dp + 4dp + 18dp + 6dp + 18dp + 10dp + 6dp + 20dp + 130dp + 120dp + 80dp 21dp -8dp 1dp @@ -110,12 +126,6 @@ 40dp 72dp 72dp - 14dp - 14dp - 24dp - 16dp - 16dp - 22sp 22sp 14dp 14dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 92692302e4ab..c60ee567ec2a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -808,6 +808,7 @@ No App available to handle maps Hide download Unread comments exist + This icon indicates availability of live photo Failed to load document! Create new document Create new spreadsheet @@ -877,8 +878,9 @@ Job name User State - Started + Created Progress + Times run in 48h Migrations (app upgrade) File transfer Remote path @@ -914,7 +916,7 @@ Failed to start editor Add folder info creates folder info - Try to upload local files again + Retry to upload failed local files We couldnt locate the file on server. Another user may have deleted the file File not found. Are you sure that this file exists or has a previous conflict not been resolved? File upload conflict