Merge remote-tracking branch 'origin/master' into dev

This commit is contained in:
Tobias Kaminsky 2023-12-22 03:36:07 +01:00
commit 05d37164ff
44 changed files with 882 additions and 506 deletions

View file

@ -24,14 +24,18 @@ jobs:
run: | run: |
if [ -z "$GITHUB_HEAD_REF" ]; then if [ -z "$GITHUB_HEAD_REF" ]; then
# push # push
echo "branch=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT" {
echo "pr=$GITHUB_RUN_ID" >> "$GITHUB_OUTPUT" echo "branch=$GITHUB_REF_NAME"
echo "repo=${{ github.repository }}" >> "$GITHUB_OUTPUT" echo "pr=$GITHUB_RUN_ID"
echo "repo=${{ github.repository }}"
} >> "$GITHUB_OUTPUT"
else else
# pull request # pull request
echo "branch=$GITHUB_HEAD_REF" >> "$GITHUB_OUTPUT" {
echo "pr=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" echo "branch=$GITHUB_HEAD_REF"
echo "repo=${{ github.event.pull_request.head.repo.full_name }}" >> "$GITHUB_OUTPUT" echo "pr=${{ github.event.pull_request.number }}"
echo "repo=${{ github.event.pull_request.head.repo.full_name }}"
} >> "$GITHUB_OUTPUT"
fi fi
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with: with:
@ -49,6 +53,6 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
mkdir -p $HOME/.gradle mkdir -p "$HOME/.gradle"
echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > $HOME/.gradle/gradle.properties 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 }} scripts/analysis/analysis-wrapper.sh ${{ steps.get-vars.outputs.branch }} ${{ secrets.LOG_USERNAME }} ${{ secrets.LOG_PASSWORD }} "$GITHUB_RUN_NUMBER" ${{ steps.get-vars.outputs.pr }}

View file

@ -26,3 +26,4 @@ jobs:
Please take a look again and update the issue with new details, Please take a look again and update the issue with new details,
otherwise the issue will be automatically closed in 2 weeks. Thank you! otherwise the issue will be automatically closed in 2 weeks. Thank you!
exempt-all-pr-milestones: true exempt-all-pr-milestones: true
labels-to-remove-when-unstale: 'needs info'

View file

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

View file

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

View file

@ -139,12 +139,10 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
val file = getDummyFile("chunkedFile.txt") val file = getDummyFile("chunkedFile.txt")
FileUploader.uploadNewFile( FileUploader.uploadNewFile(
targetContext,
user, user,
file.absolutePath, file.absolutePath,
"/testFile.txt", "/testFile.txt",
FileUploader.LOCAL_BEHAVIOUR_COPY, FileUploader.LOCAL_BEHAVIOUR_COPY,
null,
true, true,
UploadFileOperation.CREATED_BY_USER, UploadFileOperation.CREATED_BY_USER,
false, false,
@ -258,12 +256,10 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
val file = getDummyFile("nonEmpty.txt") val file = getDummyFile("nonEmpty.txt")
FileUploader.uploadNewFile( FileUploader.uploadNewFile(
targetContext,
user, user,
file.absolutePath, file.absolutePath,
"/testFile.txt", "/testFile.txt",
FileUploader.LOCAL_BEHAVIOUR_COPY, FileUploader.LOCAL_BEHAVIOUR_COPY,
null,
true, true,
UploadFileOperation.CREATED_BY_USER, UploadFileOperation.CREATED_BY_USER,
false, false,
@ -369,12 +365,10 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
val file = getDummyFile("chunkedFile.txt") val file = getDummyFile("chunkedFile.txt")
FileUploader.uploadNewFile( FileUploader.uploadNewFile(
targetContext,
user, user,
file.absolutePath, file.absolutePath,
"/testFile.txt", "/testFile.txt",
FileUploader.LOCAL_BEHAVIOUR_COPY, FileUploader.LOCAL_BEHAVIOUR_COPY,
null,
true, true,
UploadFileOperation.CREATED_BY_USER, UploadFileOperation.CREATED_BY_USER,
false, false,
@ -476,12 +470,10 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
val file = getDummyFile("chunkedFile.txt") val file = getDummyFile("chunkedFile.txt")
FileUploader.uploadNewFile( FileUploader.uploadNewFile(
targetContext,
user, user,
file.absolutePath, file.absolutePath,
"/testFile.txt", "/testFile.txt",
FileUploader.LOCAL_BEHAVIOUR_COPY, FileUploader.LOCAL_BEHAVIOUR_COPY,
null,
true, true,
UploadFileOperation.CREATED_BY_USER, UploadFileOperation.CREATED_BY_USER,
false, false,

View file

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

View file

@ -36,7 +36,6 @@ import com.owncloud.android.files.services.FileUploader
import com.owncloud.android.files.services.NameCollisionPolicy import com.owncloud.android.files.services.NameCollisionPolicy
import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.operations.UploadFileOperation
import com.owncloud.android.ui.helpers.FileOperationsHelper import com.owncloud.android.ui.helpers.FileOperationsHelper
import com.owncloud.android.utils.MimeType
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -184,16 +183,10 @@ class DocumentScanViewModel @Inject constructor(
uploadFolder + OCFile.PATH_SEPARATOR + File(it).name uploadFolder + OCFile.PATH_SEPARATOR + File(it).name
}.toTypedArray() }.toTypedArray()
val mimetypes = pageList.map {
MimeType.JPEG
}.toTypedArray()
FileUploader.uploadNewFile( FileUploader.uploadNewFile(
getApplication(),
currentAccountProvider.user, currentAccountProvider.user,
pageList.toTypedArray(), pageList.toTypedArray(),
uploadPaths, uploadPaths,
mimetypes,
FileUploader.LOCAL_BEHAVIOUR_DELETE, FileUploader.LOCAL_BEHAVIOUR_DELETE,
true, true,
UploadFileOperation.CREATED_BY_USER, UploadFileOperation.CREATED_BY_USER,

View file

@ -39,7 +39,6 @@ import com.owncloud.android.files.services.FileUploader
import com.owncloud.android.files.services.NameCollisionPolicy import com.owncloud.android.files.services.NameCollisionPolicy
import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.operations.UploadFileOperation
import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.ui.notifications.NotificationUtils
import com.owncloud.android.utils.MimeType
import com.owncloud.android.utils.theme.ViewThemeUtils import com.owncloud.android.utils.theme.ViewThemeUtils
import java.io.File import java.io.File
import java.security.SecureRandom import java.security.SecureRandom
@ -124,12 +123,10 @@ class GeneratePdfFromImagesWork(
val uploadPath = uploadFolder + OCFile.PATH_SEPARATOR + File(pdfPath).name val uploadPath = uploadFolder + OCFile.PATH_SEPARATOR + File(pdfPath).name
FileUploader.uploadNewFile( FileUploader.uploadNewFile(
appContext,
user, user,
pdfPath, pdfPath,
uploadPath, uploadPath,
FileUploader.LOCAL_BEHAVIOUR_DELETE, // MIME type will be detected from file name FileUploader.LOCAL_BEHAVIOUR_DELETE, // MIME type will be detected from file name
MimeType.PDF,
true, true,
UploadFileOperation.CREATED_BY_USER, UploadFileOperation.CREATED_BY_USER,
false, false,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -64,7 +64,8 @@ class FilesSyncWork(
private val uploadsStorageManager: UploadsStorageManager, private val uploadsStorageManager: UploadsStorageManager,
private val connectivityService: ConnectivityService, private val connectivityService: ConnectivityService,
private val powerManagementService: PowerManagementService, private val powerManagementService: PowerManagementService,
private val syncedFolderProvider: SyncedFolderProvider private val syncedFolderProvider: SyncedFolderProvider,
private val backgroundJobManager: BackgroundJobManager
) : Worker(context, params) { ) : Worker(context, params) {
companion object { companion object {
@ -74,10 +75,14 @@ class FilesSyncWork(
} }
override fun doWork(): Result { override fun doWork(): Result {
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
val overridePowerSaving = inputData.getBoolean(OVERRIDE_POWER_SAVING, false) val overridePowerSaving = inputData.getBoolean(OVERRIDE_POWER_SAVING, false)
// If we are in power save mode, better to postpone upload // If we are in power save mode, better to postpone upload
if (powerManagementService.isPowerSavingEnabled && !overridePowerSaving) { if (powerManagementService.isPowerSavingEnabled && !overridePowerSaving) {
return Result.success() val result = Result.success()
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
return result
} }
val resources = context.resources val resources = context.resources
val lightVersion = resources.getBoolean(R.bool.syncedFolder_light) val lightVersion = resources.getBoolean(R.bool.syncedFolder_light)
@ -107,7 +112,9 @@ class FilesSyncWork(
) )
} }
} }
return Result.success() val result = Result.success()
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
return result
} }
@Suppress("LongMethod") // legacy code @Suppress("LongMethod") // legacy code
@ -155,7 +162,6 @@ class FilesSyncWork(
} }
val localPaths = pathsAndMimes.map { it.first }.toTypedArray() val localPaths = pathsAndMimes.map { it.first }.toTypedArray()
val remotePaths = pathsAndMimes.map { it.second }.toTypedArray() val remotePaths = pathsAndMimes.map { it.second }.toTypedArray()
val mimetypes = pathsAndMimes.map { it.third }.toTypedArray()
if (lightVersion) { if (lightVersion) {
needsCharging = resources.getBoolean(R.bool.syncedFolder_light_on_charging) needsCharging = resources.getBoolean(R.bool.syncedFolder_light_on_charging)
@ -170,12 +176,11 @@ class FilesSyncWork(
needsWifi = syncedFolder.isWifiOnly needsWifi = syncedFolder.isWifiOnly
uploadAction = syncedFolder.uploadAction uploadAction = syncedFolder.uploadAction
} }
FileUploader.uploadNewFile( FileUploader.uploadNewFile(
context,
user, user,
localPaths, localPaths,
remotePaths, remotePaths,
mimetypes,
uploadAction!!, uploadAction!!,
true, // create parent folder if not existent true, // create parent folder if not existent
UploadFileOperation.CREATED_AS_INSTANT_PICTURE, UploadFileOperation.CREATED_AS_INSTANT_PICTURE,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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())
}
}
}

View file

@ -129,7 +129,6 @@ import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFER
* Contains methods to build the "static" strings. These strings were before constants in different classes * Contains methods to build the "static" strings. These strings were before constants in different classes
*/ */
public class MainApp extends MultiDexApplication implements HasAndroidInjector { public class MainApp extends MultiDexApplication implements HasAndroidInjector {
public static final OwnCloudVersion OUTDATED_SERVER_VERSION = NextcloudVersion.nextcloud_23; public static final OwnCloudVersion OUTDATED_SERVER_VERSION = NextcloudVersion.nextcloud_23;
public static final OwnCloudVersion MINIMUM_SUPPORTED_SERVER_VERSION = OwnCloudVersion.nextcloud_16; public static final OwnCloudVersion MINIMUM_SUPPORTED_SERVER_VERSION = OwnCloudVersion.nextcloud_16;

View file

@ -79,6 +79,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.lib.resources.files.FileUtils; 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.operations.UploadFileOperation;
import com.owncloud.android.ui.activity.ConflictsResolveActivity; import com.owncloud.android.ui.activity.ConflictsResolveActivity;
import com.owncloud.android.ui.activity.UploadListActivity; import com.owncloud.android.ui.activity.UploadListActivity;
@ -902,12 +903,10 @@ public class FileUploader extends Service
* Upload a new file * Upload a new file
*/ */
public static void uploadNewFile( public static void uploadNewFile(
Context context,
User user, User user,
String localPath, String localPath,
String remotePath, String remotePath,
int behaviour, int behaviour,
String mimeType,
boolean createRemoteFile, boolean createRemoteFile,
int createdBy, int createdBy,
boolean requiresWifi, boolean requiresWifi,
@ -915,11 +914,9 @@ public class FileUploader extends Service
NameCollisionPolicy nameCollisionPolicy NameCollisionPolicy nameCollisionPolicy
) { ) {
uploadNewFile( uploadNewFile(
context,
user, user,
new String[]{localPath}, new String[]{localPath},
new String[]{remotePath}, new String[]{remotePath},
new String[]{mimeType},
behaviour, behaviour,
createRemoteFile, createRemoteFile,
createdBy, createdBy,
@ -933,11 +930,9 @@ public class FileUploader extends Service
* Upload multiple new files * Upload multiple new files
*/ */
public static void uploadNewFile( public static void uploadNewFile(
Context context,
User user, User user,
String[] localPaths, String[] localPaths,
String[] remotePaths, String[] remotePaths,
String[] mimeTypes,
Integer behaviour, Integer behaviour,
Boolean createRemoteFolder, Boolean createRemoteFolder,
int createdBy, int createdBy,
@ -945,39 +940,15 @@ public class FileUploader extends Service
boolean requiresCharging, boolean requiresCharging,
NameCollisionPolicy nameCollisionPolicy NameCollisionPolicy nameCollisionPolicy
) { ) {
new FilesUploadHelper().uploadNewFiles(user,
localPaths,
if (useFilesUploadWorker(context)) { remotePaths,
new FilesUploadHelper().uploadNewFiles(user, createRemoteFolder,
localPaths, createdBy,
remotePaths, requiresWifi,
createRemoteFolder, requiresCharging,
createdBy, nameCollisionPolicy,
requiresWifi, behaviour);
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);
}
}
} }
/** /**
@ -1006,8 +977,7 @@ public class FileUploader extends Service
OCFile existingFile, OCFile existingFile,
Integer behaviour, Integer behaviour,
NameCollisionPolicy nameCollisionPolicy, NameCollisionPolicy nameCollisionPolicy,
boolean disableRetries boolean disableRetries) {
) {
uploadUpdateFile(context, uploadUpdateFile(context,
user, user,
new OCFile[]{existingFile}, new OCFile[]{existingFile},
@ -1036,13 +1006,7 @@ public class FileUploader extends Service
intent.putExtra(FileUploader.KEY_NAME_COLLISION_POLICY, nameCollisionPolicy); intent.putExtra(FileUploader.KEY_NAME_COLLISION_POLICY, nameCollisionPolicy);
intent.putExtra(FileUploader.KEY_DISABLE_RETRIES, disableRetries); intent.putExtra(FileUploader.KEY_DISABLE_RETRIES, disableRetries);
if (useFilesUploadWorker(context)) { new FilesUploadHelper().uploadUpdatedFile(user, existingFiles, behaviour, nameCollisionPolicy);
new FilesUploadHelper().uploadUpdatedFile(user, existingFiles, behaviour, nameCollisionPolicy);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
} }
/** /**
@ -1057,13 +1021,7 @@ public class FileUploader extends Service
i.putExtra(FileUploader.KEY_ACCOUNT, user.toPlatformAccount()); i.putExtra(FileUploader.KEY_ACCOUNT, user.toPlatformAccount());
i.putExtra(FileUploader.KEY_RETRY_UPLOAD, upload); i.putExtra(FileUploader.KEY_RETRY_UPLOAD, upload);
if (useFilesUploadWorker(context)) { new FilesUploadHelper().retryUpload(upload, user);
new FilesUploadHelper().retryUpload(upload, user);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(i);
} else {
context.startService(i);
}
} }
/** /**
@ -1129,16 +1087,6 @@ public class FileUploader extends Service
return FileUploader.class.getName() + UPLOAD_FINISH_MESSAGE; 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 @VisibleForTesting
public static void setForceNewUploadWorker(final Boolean value) { public static void setForceNewUploadWorker(final Boolean value) {
forceNewUploadWorker = value; forceNewUploadWorker = value;
@ -1150,7 +1098,6 @@ public class FileUploader extends Service
* It provides by itself the available operations. * It provides by itself the available operations.
*/ */
public class FileUploaderBinder extends Binder implements OnDatatransferProgressListener { public class FileUploaderBinder extends Binder implements OnDatatransferProgressListener {
/** /**
* Map of listeners that will be reported about progress of uploads from a {@link FileUploaderBinder} instance * Map of listeners that will be reported about progress of uploads from a {@link FileUploaderBinder} instance
*/ */
@ -1162,7 +1109,7 @@ public class FileUploader extends Service
* @param account ownCloud account where the remote file will be stored. * @param account ownCloud account where the remote file will be stored.
* @param file A file in the queue of pending uploads * @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); cancel(account.name, file.getRemotePath(), null);
} }
@ -1183,34 +1130,10 @@ public class FileUploader extends Service
* @param resultCode Setting result code will pause rather than cancel the job * @param resultCode Setting result code will pause rather than cancel the job
*/ */
public void cancel(String accountName, String remotePath, @Nullable ResultCode resultCode) { public void cancel(String accountName, String remotePath, @Nullable ResultCode resultCode) {
// Cancel for Android version >= Android 11 try {
if (useFilesUploadWorker(getApplicationContext())) { new FilesUploadHelper().cancelFileUpload(remotePath, accountManager.getUser(accountName).get());
try { } catch (NoSuchElementException e) {
new FilesUploadHelper().cancelFileUpload(remotePath, accountManager.getUser(accountName).get()); Log_OC.e(TAG, "Error cancelling current upload because user does not exist!");
} catch (NoSuchElementException e) {
Log_OC.e(TAG, "Error cancelling current upload because user does not exist!");
}
} else {
// Cancel for Android version <= Android 10
Pair<UploadFileOperation, String> 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);
}
}
} }
} }
@ -1225,14 +1148,7 @@ public class FileUploader extends Service
public void cancel(String accountName) { public void cancel(String accountName) {
cancelPendingUploads(accountName); cancelPendingUploads(accountName);
if (useFilesUploadWorker(getApplicationContext())) { new FilesUploadHelper().restartUploadJob(accountManager.getUser(accountName).get());
new FilesUploadHelper().restartUploadJob(accountManager.getUser(accountName).get());
} else {
if (mCurrentUpload != null && mCurrentUpload.getUser().nameEquals(accountName)) {
mCurrentUpload.cancel(ResultCode.CANCELLED);
}
}
} }
public void clearListeners() { public void clearListeners() {
@ -1256,43 +1172,27 @@ public class FileUploader extends Service
if (user == null || file == null) { if (user == null || file == null) {
return false; 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()); OCUpload upload = mUploadsStorageManager.getUploadByRemotePath(file.getRemotePath());
if (upload == null){
return false;
}
return upload.getUploadStatus() == UploadStatus.UPLOAD_IN_PROGRESS;
}else{ if (upload == null){
return mPendingUploads.contains(user.getAccountName(), file.getRemotePath()); return false;
} }
return upload.getUploadStatus() == UploadStatus.UPLOAD_IN_PROGRESS;
} }
@SuppressFBWarnings("NP") @SuppressFBWarnings("NP")
public boolean isUploadingNow(OCUpload upload) { public boolean isUploadingNow(OCUpload upload) {
if (useFilesUploadWorker(getApplicationContext())){ UploadFileOperation currentUploadFileOperation = FilesUploadWorker.Companion.getCurrentUploadFileOperation();
UploadFileOperation currentUploadFileOperation = FilesUploadWorker.Companion.getCurrentUploadFileOperation(); if (currentUploadFileOperation == null || currentUploadFileOperation.getUser() == null) return false;
if (currentUploadFileOperation == null || currentUploadFileOperation.getUser() == null) return false; if (upload == null || (!upload.getAccountName().equals(currentUploadFileOperation.getUser().getAccountName()))) return false;
if (upload == null || (!upload.getAccountName().equals(currentUploadFileOperation.getUser().getAccountName()))) return false; if (currentUploadFileOperation.getOldFile() != null){
if (currentUploadFileOperation.getOldFile() != null){ // For file conflicts check old file remote path
// For file conflicts check old file remote path return upload.getRemotePath().equals(currentUploadFileOperation.getRemotePath()) ||
return upload.getRemotePath().equals(currentUploadFileOperation.getRemotePath()) || upload.getRemotePath().equals(currentUploadFileOperation.getOldFile().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())));
} }
return upload.getRemotePath().equals(currentUploadFileOperation.getRemotePath());
} }
/** /**
@ -1305,18 +1205,14 @@ public class FileUploader extends Service
public void addDatatransferProgressListener( public void addDatatransferProgressListener(
OnDatatransferProgressListener listener, OnDatatransferProgressListener listener,
User user, User user,
OCFile file ServerFileInterface file
) { ) {
if (user == null || file == null || listener == null) { if (user == null || file == null || listener == null) {
return; return;
} }
String targetKey = buildRemoteName(user.getAccountName(), file.getRemotePath());
if (useFilesUploadWorker(getApplicationContext())) { String targetKey = buildRemoteName(user.getAccountName(), file.getRemotePath());
new FilesUploadHelper().addDatatransferProgressListener(listener,targetKey); new FilesUploadHelper().addDatatransferProgressListener(listener,targetKey);
}else {
mBoundListeners.put(targetKey, listener);
}
} }
/** /**
@ -1334,11 +1230,7 @@ public class FileUploader extends Service
} }
String targetKey = buildRemoteName(ocUpload.getAccountName(), ocUpload.getRemotePath()); String targetKey = buildRemoteName(ocUpload.getAccountName(), ocUpload.getRemotePath());
if (useFilesUploadWorker(getApplicationContext())) { new FilesUploadHelper().addDatatransferProgressListener(listener,targetKey);
new FilesUploadHelper().addDatatransferProgressListener(listener,targetKey);
}else {
mBoundListeners.put(targetKey, listener);
}
} }
/** /**
@ -1351,21 +1243,14 @@ public class FileUploader extends Service
public void removeDatatransferProgressListener( public void removeDatatransferProgressListener(
OnDatatransferProgressListener listener, OnDatatransferProgressListener listener,
User user, User user,
OCFile file ServerFileInterface file
) { ) {
if (user == null || file == null || listener == null) { if (user == null || file == null || listener == null) {
return; return;
} }
String targetKey = buildRemoteName(user.getAccountName(), file.getRemotePath()); String targetKey = buildRemoteName(user.getAccountName(), file.getRemotePath());
new FilesUploadHelper().removeDatatransferProgressListener(listener,targetKey);
if (useFilesUploadWorker(getApplicationContext())) {
new FilesUploadHelper().removeDatatransferProgressListener(listener,targetKey);
}else {
if (mBoundListeners.get(targetKey) == listener) {
mBoundListeners.remove(targetKey);
}
}
} }
/** /**
@ -1383,14 +1268,7 @@ public class FileUploader extends Service
} }
String targetKey = buildRemoteName(ocUpload.getAccountName(), ocUpload.getRemotePath()); String targetKey = buildRemoteName(ocUpload.getAccountName(), ocUpload.getRemotePath());
new FilesUploadHelper().removeDatatransferProgressListener(listener,targetKey);
if (useFilesUploadWorker(getApplicationContext())) {
new FilesUploadHelper().removeDatatransferProgressListener(listener,targetKey);
}else {
if (mBoundListeners.get(targetKey) == listener) {
mBoundListeners.remove(targetKey);
}
}
} }
@Override @Override

View file

@ -906,8 +906,15 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
default -> FileUploader.LOCAL_BEHAVIOUR_FORGET; default -> FileUploader.LOCAL_BEHAVIOUR_FORGET;
}; };
FileUploader.uploadNewFile(this, getUser().orElseThrow(RuntimeException::new), filePaths, remotePaths, null, // MIME type will be detected from file name FileUploader.uploadNewFile(getUser().orElseThrow(RuntimeException::new),
behaviour, true, UploadFileOperation.CREATED_BY_USER, false, false, NameCollisionPolicy.ASK_USER); filePaths,
remotePaths,
behaviour,
true,
UploadFileOperation.CREATED_BY_USER,
false,
false,
NameCollisionPolicy.ASK_USER);
} else { } else {
Log_OC.d(TAG, "User clicked on 'Update' with no selection"); Log_OC.d(TAG, "User clicked on 'Update' with no selection");
@ -1471,6 +1478,7 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
setFile(listOfFiles.getCurrentFile()); setFile(listOfFiles.getCurrentFile());
startSyncFolderOperation(root, false); startSyncFolderOperation(root, false);
} }
binding.fabMain.setImageResource(R.drawable.ic_plus);
resetTitleBarAndScrolling(); resetTitleBarAndScrolling();
} }

View file

@ -885,12 +885,10 @@ public class ReceiveExternalFilesActivity extends FileActivity
public void uploadFile(String tmpName, String filename) { public void uploadFile(String tmpName, String filename) {
FileUploader.uploadNewFile( FileUploader.uploadNewFile(
getBaseContext(),
getUser().orElseThrow(RuntimeException::new), getUser().orElseThrow(RuntimeException::new),
tmpName, tmpName,
mFile.getRemotePath() + filename, mFile.getRemotePath() + filename,
FileUploader.LOCAL_BEHAVIOUR_COPY, FileUploader.LOCAL_BEHAVIOUR_COPY,
null,
true, true,
UploadFileOperation.CREATED_BY_USER, UploadFileOperation.CREATED_BY_USER,
false, false,

View file

@ -196,19 +196,22 @@ public class UploadListActivity extends FileActivity {
private void refresh() { private void refresh() {
backgroundJobManager.startImmediateFilesSyncJob(false, true); backgroundJobManager.startImmediateFilesSyncJob(false, true);
// retry failed uploads if(uploadsStorageManager.getFailedUploads().length > 0){
new Thread(() -> FileUploader.retryFailedUploads( // retry failed uploads
this, new Thread(() -> FileUploader.retryFailedUploads(
uploadsStorageManager, this,
connectivityService, uploadsStorageManager,
userAccountManager, connectivityService,
powerManagementService)) userAccountManager,
.start(); powerManagementService))
.start();
DisplayUtils.showSnackMessage(this, R.string.uploader_local_files_uploaded);
}
// update UI // update UI
uploadListAdapter.loadUploadItemsFromDb(); uploadListAdapter.loadUploadItemsFromDb();
swipeListRefreshLayout.setRefreshing(false); swipeListRefreshLayout.setRefreshing(false);
DisplayUtils.showSnackMessage(this, R.string.uploader_local_files_uploaded);
} }
@Override @Override

View file

@ -22,7 +22,9 @@
package com.owncloud.android.ui.adapter package com.owncloud.android.ui.adapter
import android.view.View import android.view.View
import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import com.elyeproj.loaderviewlibrary.LoaderImageView import com.elyeproj.loaderviewlibrary.LoaderImageView
@ -32,12 +34,14 @@ interface ListGridImageViewHolder {
val shimmerThumbnail: LoaderImageView val shimmerThumbnail: LoaderImageView
val favorite: ImageView val favorite: ImageView
val localFileIndicator: ImageView val localFileIndicator: ImageView
val imageFileName: TextView?
val shared: ImageView val shared: ImageView
val checkbox: ImageView val checkbox: ImageView
val itemLayout: View val itemLayout: View
val unreadComments: ImageView val unreadComments: ImageView
val more: ImageButton?
val gridLivePhotoIndicator: TextView? val fileFeaturesLayout: LinearLayout?
val gridLivePhotoIndicator: ImageView?
val livePhotoIndicator: TextView? val livePhotoIndicator: TextView?
val livePhotoIndicatorSeparator: TextView? val livePhotoIndicatorSeparator: TextView?
} }

View file

@ -32,6 +32,7 @@ import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import com.nextcloud.android.common.ui.theme.utils.ColorRole;
import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.client.preferences.AppPreferences;
import com.owncloud.android.R; import com.owncloud.android.R;
import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.datamodel.ThumbnailsCacheManager;
@ -54,6 +55,7 @@ import java.util.concurrent.Executors;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
/** /**
@ -186,11 +188,10 @@ public class LocalFileListAdapter extends RecyclerView.Adapter<RecyclerView.View
} else { } else {
gridViewHolder.checkbox.setVisibility(View.VISIBLE); gridViewHolder.checkbox.setVisibility(View.VISIBLE);
if (isCheckedFile(file)) { if (isCheckedFile(file)) {
gridViewHolder.itemLayout.setBackgroundColor(mContext.getResources() gridViewHolder.itemLayout.setBackgroundColor(ContextCompat.getColor(mContext, R.color.selected_item_background));
.getColor(R.color.selected_item_background));
gridViewHolder.checkbox.setImageDrawable( gridViewHolder.checkbox.setImageDrawable(
viewThemeUtils.platform.tintPrimaryDrawable(mContext, R.drawable.ic_checkbox_marked)); viewThemeUtils.platform.tintDrawable(mContext, R.drawable.ic_checkbox_marked, ColorRole.PRIMARY));
} else { } else {
gridViewHolder.itemLayout.setBackgroundColor(mContext.getResources().getColor(R.color.bg_default)); gridViewHolder.itemLayout.setBackgroundColor(mContext.getResources().getColor(R.color.bg_default));
gridViewHolder.checkbox.setImageResource(R.drawable.ic_checkbox_blank_outline); gridViewHolder.checkbox.setImageResource(R.drawable.ic_checkbox_blank_outline);

View file

@ -39,7 +39,9 @@ import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import com.elyeproj.loaderviewlibrary.LoaderImageView; import com.elyeproj.loaderviewlibrary.LoaderImageView;
import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.android.common.ui.theme.utils.ColorRole;
@ -300,6 +302,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
return headerId; return headerId;
} }
// skip header // skip header
position--; position--;
} }
@ -365,8 +368,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
@Override @Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
switch (viewType) { switch (viewType) {
default: default -> {
case VIEWTYPE_ITEM:
if (gridView) { if (gridView) {
return new OCFileListGridItemViewHolder( return new OCFileListGridItemViewHolder(
GridItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) GridItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)
@ -376,8 +378,8 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
ListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) ListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)
); );
} }
}
case VIEWTYPE_IMAGE: case VIEWTYPE_IMAGE -> {
if (gridView) { if (gridView) {
return new OCFileListGridImageViewHolder( return new OCFileListGridImageViewHolder(
GridImageBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) GridImageBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)
@ -387,23 +389,22 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
ListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) ListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)
); );
} }
}
case VIEWTYPE_FOOTER: case VIEWTYPE_FOOTER -> {
return new OCFileListFooterViewHolder( return new OCFileListFooterViewHolder(
ListFooterBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) ListFooterBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)
); );
}
case VIEWTYPE_HEADER: case VIEWTYPE_HEADER -> {
ListHeaderBinding binding = ListHeaderBinding.inflate( ListHeaderBinding binding = ListHeaderBinding.inflate(
LayoutInflater.from(parent.getContext()), LayoutInflater.from(parent.getContext()),
parent, parent,
false); false);
ViewGroup.LayoutParams layoutParams = binding.headerView.getLayoutParams(); ViewGroup.LayoutParams layoutParams = binding.headerView.getLayoutParams();
layoutParams.height = (int) (parent.getHeight() * 0.3); layoutParams.height = (int) (parent.getHeight() * 0.3);
binding.headerView.setLayoutParams(layoutParams); binding.headerView.setLayoutParams(layoutParams);
return new OCFileListHeaderViewHolder(binding); return new OCFileListHeaderViewHolder(binding);
}
} }
} }
@ -430,6 +431,8 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
} }
ocFileListDelegate.bindGridViewHolder(gridViewHolder, file, searchType); ocFileListDelegate.bindGridViewHolder(gridViewHolder, file, searchType);
checkVisibilityOfMoreButtons(gridViewHolder);
checkVisibilityOfFileFeaturesLayout(gridViewHolder);
if (holder instanceof ListItemViewHolder) { if (holder instanceof ListItemViewHolder) {
bindListItemViewHolder((ListItemViewHolder) gridViewHolder, file); bindListItemViewHolder((ListItemViewHolder) gridViewHolder, file);
@ -437,12 +440,45 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
if (holder instanceof ListGridItemViewHolder) { if (holder instanceof ListGridItemViewHolder) {
bindListGridItemViewHolder((ListGridItemViewHolder) holder, file); bindListGridItemViewHolder((ListGridItemViewHolder) holder, file);
checkVisibilityOfMoreButtons((ListGridItemViewHolder) holder);
checkVisibilityOfFileFeaturesLayout((ListGridItemViewHolder) holder);
} }
updateLivePhotoIndicators(gridViewHolder, file); 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() { private void mergeOCFilesForLivePhoto() {
List<OCFile> filesToRemove = new ArrayList<>(); List<OCFile> filesToRemove = new ArrayList<>();

View file

@ -27,10 +27,13 @@ import android.graphics.drawable.ColorDrawable
import android.os.AsyncTask import android.os.AsyncTask
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import com.elyeproj.loaderviewlibrary.LoaderImageView import com.elyeproj.loaderviewlibrary.LoaderImageView
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.client.account.User import com.nextcloud.client.account.User
import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.utils.extensions.createRoundedOutline
import com.owncloud.android.R import com.owncloud.android.R
import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.OCFile
@ -142,7 +145,7 @@ class OCFileListDelegate(
storageManager, storageManager,
asyncGalleryTasks, asyncGalleryTasks,
file.remoteId, file.remoteId,
context.resources.getColor(R.color.bg_default) ContextCompat.getColor(context, R.color.bg_default)
) )
var drawable = MimeTypeUtil.getFileTypeIcon( var drawable = MimeTypeUtil.getFileTypeIcon(
file.mimeType, file.mimeType,
@ -204,6 +207,7 @@ class OCFileListDelegate(
searchType: SearchType? searchType: SearchType?
) { ) {
// thumbnail // thumbnail
gridViewHolder.imageFileName?.text = file.fileName
gridViewHolder.thumbnail.tag = file.fileId gridViewHolder.thumbnail.tag = file.fileId
DisplayUtils.setThumbnail( DisplayUtils.setThumbnail(
file, file,
@ -218,6 +222,7 @@ class OCFileListDelegate(
viewThemeUtils, viewThemeUtils,
syncFolderProvider syncFolderProvider
) )
// item layout + click listeners // item layout + click listeners
bindGridItemLayout(file, gridViewHolder) bindGridItemLayout(file, gridViewHolder)
@ -232,17 +237,20 @@ class OCFileListDelegate(
} }
// download state // download state
gridViewHolder.localFileIndicator.visibility = View.INVISIBLE // default first gridViewHolder.localFileIndicator.visibility = View.GONE // default first
// metadata (downloaded, favorite) // metadata (downloaded, favorite)
bindGridMetadataViews(file, gridViewHolder) bindGridMetadataViews(file, gridViewHolder)
// shares // shares
val shouldHideShare = gridView || val shouldHideShare = (
hideItemOptions || hideItemOptions ||
!file.isFolder && file.isEncrypted || !file.isFolder &&
file.isEncrypted && !EncryptionUtils.supportsSecureFiledrop(file, user) || file.isEncrypted ||
searchType == SearchType.FAVORITE_SEARCH file.isEncrypted &&
!EncryptionUtils.supportsSecureFiledrop(file, user) ||
searchType == SearchType.FAVORITE_SEARCH
)
if (shouldHideShare) { if (shouldHideShare) {
gridViewHolder.shared.visibility = View.GONE gridViewHolder.shared.visibility = View.GONE
} else { } else {
@ -263,32 +271,61 @@ class OCFileListDelegate(
} }
private fun bindGridItemLayout(file: OCFile, gridViewHolder: ListGridImageViewHolder) { private fun bindGridItemLayout(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
if (highlightedItem != null && file.fileId == highlightedItem!!.fileId) { setItemLayoutBackgroundColor(file, gridViewHolder)
gridViewHolder.itemLayout.setBackgroundColor( setCheckBoxImage(file, gridViewHolder)
context.resources setItemLayoutOnClickListeners(file, gridViewHolder)
.getColor(R.color.selected_item_background)
) gridViewHolder.more?.setOnClickListener {
} else if (isCheckedFile(file)) { ocFileListFragmentInterface.onOverflowIconClicked(file, it)
gridViewHolder.itemLayout.setBackgroundColor( }
context.resources }
.getColor(R.color.selected_item_background)
) private fun setItemLayoutOnClickListeners(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
gridViewHolder.itemLayout.setOnClickListener { ocFileListFragmentInterface.onItemClicked(file) }
if (!hideItemOptions) {
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( gridViewHolder.checkbox.setImageDrawable(
viewThemeUtils.platform.tintPrimaryDrawable(context, R.drawable.ic_checkbox_marked) viewThemeUtils.platform.tintDrawable(context, R.drawable.ic_checkbox_marked, ColorRole.PRIMARY)
) )
} else { } else {
gridViewHolder.itemLayout.setBackgroundColor(context.resources.getColor(R.color.bg_default))
gridViewHolder.checkbox.setImageResource(R.drawable.ic_checkbox_blank_outline) gridViewHolder.checkbox.setImageResource(R.drawable.ic_checkbox_blank_outline)
} }
gridViewHolder.itemLayout.setOnClickListener { ocFileListFragmentInterface.onItemClicked(file) }
if (!hideItemOptions) {
gridViewHolder.itemLayout.isLongClickable = true
gridViewHolder.itemLayout.setOnLongClickListener {
ocFileListFragmentInterface.onLongItemClicked(
file
)
}
}
} }
private fun bindGridMetadataViews(file: OCFile, gridViewHolder: ListGridImageViewHolder) { private fun bindGridMetadataViews(file: OCFile, gridViewHolder: ListGridImageViewHolder) {

View file

@ -22,7 +22,9 @@
package com.owncloud.android.ui.adapter package com.owncloud.android.ui.adapter
import android.view.View import android.view.View
import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.elyeproj.loaderviewlibrary.LoaderImageView import com.elyeproj.loaderviewlibrary.LoaderImageView
@ -33,9 +35,13 @@ internal class OCFileListGridImageViewHolder(var binding: GridImageBinding) :
binding.root binding.root
), ),
ListGridImageViewHolder { ListGridImageViewHolder {
override val thumbnail: ImageView override val thumbnail: ImageView
get() = binding.thumbnail get() = binding.thumbnail
override val imageFileName: TextView
get() = binding.Filename
override fun showVideoOverlay() { override fun showVideoOverlay() {
// noop // noop
} }
@ -54,8 +60,11 @@ internal class OCFileListGridImageViewHolder(var binding: GridImageBinding) :
get() = binding.ListItemLayout get() = binding.ListItemLayout
override val unreadComments: ImageView override val unreadComments: ImageView
get() = binding.unreadComments get() = binding.unreadComments
override val more: ImageButton
override val gridLivePhotoIndicator: TextView get() = binding.more
override val fileFeaturesLayout: LinearLayout
get() = binding.fileFeaturesLayout
override val gridLivePhotoIndicator: ImageView
get() = binding.gridLivePhotoIndicator get() = binding.gridLivePhotoIndicator
override val livePhotoIndicator: TextView? override val livePhotoIndicator: TextView?
get() = null get() = null

View file

@ -22,7 +22,9 @@
package com.owncloud.android.ui.adapter package com.owncloud.android.ui.adapter
import android.view.View import android.view.View
import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.elyeproj.loaderviewlibrary.LoaderImageView import com.elyeproj.loaderviewlibrary.LoaderImageView
@ -48,6 +50,8 @@ internal class OCFileListGridItemViewHolder(var binding: GridItemBinding) :
get() = binding.favoriteAction get() = binding.favoriteAction
override val localFileIndicator: ImageView override val localFileIndicator: ImageView
get() = binding.localFileIndicator get() = binding.localFileIndicator
override val imageFileName: TextView?
get() = null
override val shared: ImageView override val shared: ImageView
get() = binding.sharedIcon get() = binding.sharedIcon
override val checkbox: ImageView override val checkbox: ImageView
@ -57,12 +61,16 @@ internal class OCFileListGridItemViewHolder(var binding: GridItemBinding) :
override val unreadComments: ImageView override val unreadComments: ImageView
get() = binding.unreadComments get() = binding.unreadComments
override val gridLivePhotoIndicator: TextView? override val gridLivePhotoIndicator: ImageView?
get() = null get() = null
override val livePhotoIndicator: TextView? override val livePhotoIndicator: TextView?
get() = null get() = null
override val livePhotoIndicatorSeparator: TextView? override val livePhotoIndicatorSeparator: TextView?
get() = null get() = null
override val fileFeaturesLayout: LinearLayout
get() = binding.fileFeaturesLayout
override val more: ImageButton
get() = binding.more
init { init {
binding.favoriteAction.drawable.mutate() binding.favoriteAction.drawable.mutate()

View file

@ -22,6 +22,7 @@
package com.owncloud.android.ui.adapter package com.owncloud.android.ui.adapter
import android.view.View import android.view.View
import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
@ -37,7 +38,7 @@ internal class OCFileListItemViewHolder(private var binding: ListItemBinding) :
binding.root binding.root
), ),
ListItemViewHolder { ListItemViewHolder {
override val gridLivePhotoIndicator: TextView? override val gridLivePhotoIndicator: ImageView?
get() = null get() = null
override val livePhotoIndicator: TextView override val livePhotoIndicator: TextView
get() = binding.livePhotoIndicator get() = binding.livePhotoIndicator
@ -73,12 +74,18 @@ internal class OCFileListItemViewHolder(private var binding: ListItemBinding) :
binding.thumbnailLayout.videoOverlay.visibility = View.VISIBLE binding.thumbnailLayout.videoOverlay.visibility = View.VISIBLE
} }
override val more: ImageButton?
get() = null
override val fileFeaturesLayout: LinearLayout?
get() = null
override val shimmerThumbnail: LoaderImageView override val shimmerThumbnail: LoaderImageView
get() = binding.thumbnailLayout.thumbnailShimmer get() = binding.thumbnailLayout.thumbnailShimmer
override val favorite: ImageView override val favorite: ImageView
get() = binding.favoriteAction get() = binding.favoriteAction
override val localFileIndicator: ImageView override val localFileIndicator: ImageView
get() = binding.localFileIndicator get() = binding.localFileIndicator
override val imageFileName: TextView?
get() = null
override val shared: ImageView override val shared: ImageView
get() = binding.sharedIcon get() = binding.sharedIcon
override val checkbox: ImageView override val checkbox: ImageView

View file

@ -192,8 +192,7 @@ public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, Result
user, user,
fullTempPath, fullTempPath,
currentRemotePath, currentRemotePath,
behaviour, behaviour
leakedContentResolver.getType(currentUri)
); );
fullTempPath = null; fullTempPath = null;
} }
@ -247,14 +246,12 @@ public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, Result
return result; 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( FileUploader.uploadNewFile(
mAppContext,
user, user,
localPath, localPath,
remotePath, remotePath,
behaviour, behaviour,
mimeType,
false, // do not create parent folder if not existent false, // do not create parent folder if not existent
UploadFileOperation.CREATED_BY_USER, UploadFileOperation.CREATED_BY_USER,
false, false,

View file

@ -126,12 +126,10 @@ class UriUploader(
*/ */
private fun requestUpload(localPath: String?, remotePath: String) { private fun requestUpload(localPath: String?, remotePath: String) {
FileUploader.uploadNewFile( FileUploader.uploadNewFile(
mActivity,
user, user,
localPath, localPath,
remotePath, remotePath,
mBehaviour, mBehaviour,
null, // MIME type will be detected from file name
false, // do not create parent folder if not existent false, // do not create parent folder if not existent
UploadFileOperation.CREATED_BY_USER, UploadFileOperation.CREATED_BY_USER,
false, false,

View file

@ -977,9 +977,9 @@ public final class DisplayUtils {
} }
private static void configShimmerGridImageSize(LoaderImageView thumbnailShimmer, float gridColumns) { private static void configShimmerGridImageSize(LoaderImageView thumbnailShimmer, float gridColumns) {
FrameLayout.LayoutParams targetLayoutParams = (FrameLayout.LayoutParams) thumbnailShimmer.getLayoutParams();
try { try {
FrameLayout.LayoutParams targetLayoutParams = (FrameLayout.LayoutParams) thumbnailShimmer.getLayoutParams();
final Point screenSize = getScreenSize(thumbnailShimmer.getContext()); final Point screenSize = getScreenSize(thumbnailShimmer.getContext());
final int marginLeftAndRight = targetLayoutParams.leftMargin + targetLayoutParams.rightMargin; final int marginLeftAndRight = targetLayoutParams.leftMargin + targetLayoutParams.rightMargin;
final int size = Math.round(screenSize.x / gridColumns - marginLeftAndRight); final int size = Math.round(screenSize.x / gridColumns - marginLeftAndRight);

View file

@ -151,7 +151,6 @@ class FilesUploadHelper {
val boundListener = mBoundListeners[key] val boundListener = mBoundListeners[key]
boundListener?.onTransferProgress(progressRate, totalTransferredSoFar, totalToTransfer, fileName) boundListener?.onTransferProgress(progressRate, totalTransferredSoFar, totalToTransfer, fileName)
Log_OC.d("TAG", "Hello")
} }
} }
} }

View file

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

View file

@ -15,129 +15,176 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/ListItemLayout" android:id="@+id/ListItemLayout"
android:orientation="vertical"
android:layout_marginEnd="@dimen/grid_container_margin"
android:layout_marginTop="@dimen/grid_container_margin"
android:gravity="center"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="@dimen/grid_container_height">
android:layout_gravity="center_horizontal"
android:foreground="?android:attr/selectableItemBackground"
android:gravity="center_horizontal"
android:orientation="vertical">
<com.elyeproj.loaderviewlibrary.LoaderImageView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/thumbnail_shimmer" android:layout_width="@dimen/grid_container_width"
android:layout_width="match_parent" android:layout_height="@dimen/grid_container_height">
android:layout_height="match_parent"
android:layout_margin="@dimen/grid_image_icon_margin"
android:contentDescription="@null"
android:visibility="gone"
app:corners="6"
app:height_weight="0.6"
app:width_weight="0.4" />
<com.owncloud.android.ui.SquareImageView <com.elyeproj.loaderviewlibrary.LoaderImageView
android:id="@+id/thumbnail" android:id="@+id/thumbnail_shimmer"
android:layout_width="match_parent" android:visibility="gone"
android:layout_height="match_parent" android:layout_marginBottom="@dimen/grid_thumbnail_margin_bottom"
android:contentDescription="@null" android:layout_width="@dimen/standard_list_item_size"
android:padding="@dimen/grid_image_icon_padding" android:layout_height="@dimen/standard_list_item_size"
android:scaleType="centerCrop" android:contentDescription="@null"
android:src="@drawable/file_image" /> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<ImageView <com.owncloud.android.ui.SquareImageView
tools:visibility="visible" android:id="@+id/thumbnail"
android:id="@+id/favorite_action" android:layout_marginBottom="@dimen/grid_thumbnail_margin_bottom"
android:layout_width="16dp" android:layout_width="@dimen/standard_list_item_size"
android:layout_height="16dp" android:layout_height="@dimen/standard_list_item_size"
android:layout_gravity="top|end" android:contentDescription="@null"
android:layout_margin="@dimen/standard_quarter_margin" app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/favorite_icon" app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone" app:layout_constraintStart_toStartOf="parent"
android:src="@drawable/favorite" /> app:layout_constraintTop_toTopOf="parent"
android:scaleType="centerCrop"
<ImageView
tools:visibility="visible"
android:id="@+id/unreadComments"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginTop="70dp"
android:layout_marginEnd="@dimen/standard_quarter_margin"
android:clickable="true"
android:contentDescription="@string/unread_comments"
android:focusable="true"
android:src="@drawable/ic_comment_grid"
android:visibility="gone" />
<ImageView
tools:visibility="visible"
android:id="@+id/localFileIndicator"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="bottom|end"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:layout_marginEnd="@dimen/standard_quarter_margin"
android:layout_marginBottom="@dimen/standard_quarter_margin"
android:contentDescription="@string/synced_icon"
android:src="@drawable/ic_synced" />
<FrameLayout
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="top|end"
android:layout_marginTop="32dp"
android:layout_marginEnd="@dimen/standard_quarter_margin"
android:background="@drawable/rounded_rect"
android:backgroundTint="#F6F6F6">
<ImageView
android:id="@+id/sharedIcon"
tools:visibility="visible" tools:visibility="visible"
android:layout_gravity="center" android:src="@drawable/file_image" />
<LinearLayout
android:id="@+id/file_features_layout"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/shared_icon_shared_via_link" android:gravity="center"
android:src="@drawable/shared_via_link" /> 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">
</FrameLayout> <ImageView
android:id="@+id/favorite_action"
android:layout_width="@dimen/grid_layout_item_size"
android:layout_height="@dimen/grid_layout_item_size"
android:layout_marginEnd="@dimen/grid_layout_margin_end"
android:contentDescription="@string/favorite_icon"
android:src="@drawable/favorite" />
<TextView <ImageView
android:id="@+id/grid_live_photo_indicator" android:id="@+id/videoOverlay"
tools:visibility="visible" android:layout_width="@dimen/grid_layout_item_size"
android:visibility="gone" android:layout_height="@dimen/grid_layout_item_size"
android:layout_width="wrap_content" android:layout_marginEnd="@dimen/grid_layout_margin_end"
android:layout_height="wrap_content" android:contentDescription="@string/video_overlay_icon"
app:drawableTint="@color/list_item_lastmod_and_filesize_text" android:src="@drawable/video_white"
android:background="@drawable/rounded_rect" android:visibility="gone"
android:backgroundTint="#F6F6F6" app:tint="@color/grid_file_features_icon_color"
android:paddingVertical="10dp" tools:visibility="visible" />
android:paddingHorizontal="2dp"
android:layout_marginLeft="@dimen/standard_quarter_margin"
android:layout_marginRight="@dimen/standard_quarter_margin"
android:text="@string/file_list_live"
android:drawablePadding="@dimen/standard_eight_padding"
android:gravity="center"
android:layout_gravity="start|bottom"
android:textColor="@color/list_item_lastmod_and_filesize_text"
android:textSize="@dimen/two_line_secondary_text_size"
app:drawableTopCompat="@drawable/ic_live_photo" />
<TextView <ImageView
android:id="@+id/live_photo_indicator_separator" android:id="@+id/sharedIcon"
android:visibility="gone" android:layout_width="@dimen/grid_layout_item_size"
android:layout_width="0dp" android:layout_height="@dimen/grid_layout_item_size"
android:layout_height="0dp"/> android:layout_marginEnd="@dimen/grid_layout_margin_end"
android:contentDescription="@string/shared_icon_shared_via_link"
android:src="@drawable/shared_via_link"
app:tint="@color/grid_file_features_icon_color"
tools:visibility="visible" />
<ImageView <ImageView
tools:visibility="visible" android:id="@+id/unreadComments"
android:id="@+id/custom_checkbox" tools:ignore="TouchTargetSizeCheck"
android:layout_width="wrap_content" android:layout_width="@dimen/grid_layout_item_size"
android:layout_height="wrap_content" android:layout_height="@dimen/grid_layout_item_size"
android:layout_gravity="center_vertical|top" android:layout_marginEnd="@dimen/grid_layout_margin_end"
android:layout_marginLeft="@dimen/standard_quarter_margin" android:clickable="true"
android:layout_marginRight="@dimen/standard_quarter_margin" android:contentDescription="@string/unread_comments"
android:contentDescription="@string/checkbox" android:focusable="true"
android:src="@android:drawable/checkbox_off_background" /> android:src="@drawable/ic_comment_grid"
</FrameLayout> android:visibility="gone"
app:tint="@color/grid_file_features_icon_color"
tools:visibility="visible" />
<ImageView
android:id="@+id/grid_live_photo_indicator"
tools:ignore="TouchTargetSizeCheck"
android:layout_width="@dimen/grid_layout_item_size"
android:layout_height="@dimen/grid_layout_item_size"
android:layout_marginEnd="@dimen/grid_layout_margin_end"
android:clickable="true"
android:contentDescription="@string/grid_file_features_live_photo_content_description"
android:focusable="true"
android:src="@drawable/ic_live_photo"
android:visibility="gone"
app:tint="@color/grid_file_features_icon_color"
tools:visibility="visible" />
<ImageView
android:id="@+id/localFileIndicator"
android:layout_width="@dimen/grid_layout_item_size"
android:layout_height="@dimen/grid_layout_item_size"
android:layout_marginEnd="@dimen/grid_layout_margin_end"
android:contentDescription="@string/synced_icon"
android:src="@drawable/ic_synced"
tools:visibility="visible" />
</LinearLayout>
<ImageView
android:id="@+id/custom_checkbox"
android:layout_width="@dimen/grid_checkbox_size"
android:layout_height="@dimen/grid_checkbox_size"
android:layout_marginStart="@dimen/grid_checkbox_margin"
android:layout_marginTop="@dimen/grid_checkbox_margin"
android:contentDescription="@string/checkbox"
android:src="@android:drawable/checkbox_off_background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<TextView
android:id="@+id/Filename"
android:layout_width="@dimen/grid_filename_width"
android:layout_height="@dimen/grid_bottom_view_height"
android:layout_gravity="center"
android:layout_marginBottom="@dimen/grid_bottom_view_margin_bottom"
android:ellipsize="middle"
android:gravity="center"
android:singleLine="true"
android:text="@string/placeholder_filename"
android:textColor="@color/text_color"
android:textSize="@dimen/grid_item_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<ImageButton
android:id="@+id/more"
android:layout_width="@dimen/grid_bottom_view_height"
android:layout_marginBottom="@dimen/grid_bottom_view_margin_bottom"
tools:ignore="TouchTargetSizeCheck"
android:layout_height="@dimen/grid_bottom_view_height"
android:layout_marginEnd="@dimen/grid_bottom_view_margin_end"
android:layout_gravity="center"
android:background="@color/transparent"
android:contentDescription="@string/overflow_menu"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_dots_vertical" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View file

@ -15,130 +15,164 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<com.owncloud.android.ui.SquareLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/ListItemLayout" android:id="@+id/ListItemLayout"
android:layout_width="match_parent" android:layout_marginEnd="@dimen/grid_container_margin"
android:layout_height="wrap_content" android:layout_marginTop="@dimen/grid_container_margin"
android:layout_gravity="center_horizontal" android:orientation="vertical"
android:gravity="center" android:gravity="center"
android:orientation="vertical"> android:layout_width="match_parent"
android:layout_height="@dimen/grid_container_height">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="@dimen/grid_container_width"
android:layout_height="wrap_content"> android:layout_height="@dimen/grid_container_height">
<com.elyeproj.loaderviewlibrary.LoaderImageView
android:id="@+id/thumbnail_shimmer"
android:visibility="gone"
android:layout_marginBottom="@dimen/grid_thumbnail_margin_bottom"
android:layout_width="@dimen/standard_list_item_size"
android:layout_height="@dimen/standard_list_item_size"
android:contentDescription="@null"
android:src="@drawable/folder"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<ImageView <ImageView
android:id="@+id/favorite_action" android:id="@+id/thumbnail"
android:layout_width="@dimen/standard_list_item_size"
android:layout_height="@dimen/standard_list_item_size"
android:layout_marginBottom="@dimen/grid_thumbnail_margin_bottom"
android:contentDescription="@null"
android:src="@drawable/folder"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/file_features_layout"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="top|end" android:gravity="center"
android:layout_margin="@dimen/standard_quarter_margin" android:layout_marginEnd="@dimen/grid_layout_file_features_margin_end"
android:src="@drawable/favorite" android:layout_marginBottom="@dimen/grid_layout_margin_bottom"
android:contentDescription="@string/favorite_icon" android:alpha="0.9"
app:layout_constraintEnd_toEndOf="@+id/frameLayout" android:background="@drawable/rounded_rect"
app:layout_constraintTop_toTopOf="@id/frameLayout" android:backgroundTint="@color/grid_file_features_background_color"
app:layout_constraintBottom_toTopOf="@id/frameLayout"/> android:orientation="horizontal"
android:padding="@dimen/standard_quarter_padding"
android:translationZ="4dp"
app:layout_constraintBottom_toBottomOf="@+id/thumbnail"
app:layout_constraintEnd_toEndOf="parent">
<FrameLayout <ImageView
android:id="@+id/frameLayout" tools:visibility="visible"
android:layout_width="match_parent" android:id="@+id/favorite_action"
android:layout_height="wrap_content" android:layout_width="@dimen/grid_layout_item_size"
android:layout_gravity="center_horizontal" android:layout_height="@dimen/grid_layout_item_size"
app:layout_constraintBottom_toBottomOf="parent"> android:layout_marginEnd="@dimen/grid_layout_margin_end"
android:contentDescription="@string/favorite_icon"
android:src="@drawable/favorite" />
<com.elyeproj.loaderviewlibrary.LoaderImageView <ImageView
android:id="@+id/thumbnail_shimmer" android:id="@+id/videoOverlay"
android:layout_width="@dimen/standard_list_item_size" android:layout_width="@dimen/grid_layout_item_size"
android:layout_height="@dimen/standard_list_item_size" android:layout_height="@dimen/grid_layout_item_size"
android:layout_gravity="center" android:layout_marginEnd="@dimen/grid_layout_margin_end"
android:contentDescription="@string/video_overlay_icon"
android:src="@drawable/video_white"
android:visibility="gone" android:visibility="gone"
app:corners="8" /> app:tint="@color/grid_file_features_icon_color"
tools:visibility="visible" />
<FrameLayout
android:layout_width="@dimen/standard_list_item_size"
android:layout_height="@dimen/standard_list_item_size"
android:layout_gravity="center">
<ImageView
android:id="@+id/thumbnail"
android:layout_width="@dimen/standard_list_item_size"
android:layout_height="@dimen/standard_list_item_size"
android:contentDescription="@null"
android:src="@drawable/folder" />
<ImageView
android:id="@+id/videoOverlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:contentDescription="@string/video_overlay_icon"
android:src="@drawable/video_white"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
<ImageView <ImageView
android:id="@+id/sharedIcon" android:id="@+id/sharedIcon"
android:layout_width="wrap_content" android:layout_width="@dimen/grid_layout_item_size"
android:layout_height="wrap_content" android:layout_height="@dimen/grid_layout_item_size"
android:layout_gravity="top|end" android:layout_marginEnd="@dimen/grid_layout_margin_end"
android:layout_marginTop="@dimen/grid_item_shared_icon_layout_top_margin"
android:layout_marginEnd="@dimen/standard_quarter_margin"
android:contentDescription="@string/shared_icon_shared_via_link" android:contentDescription="@string/shared_icon_shared_via_link"
android:src="@drawable/shared_via_link" /> android:src="@drawable/shared_via_link"
app:tint="@color/grid_file_features_icon_color"
tools:visibility="visible" />
<ImageView <ImageView
android:id="@+id/unreadComments" android:id="@+id/unreadComments"
android:layout_width="wrap_content" android:layout_width="@dimen/grid_layout_item_size"
android:layout_height="wrap_content" android:layout_height="@dimen/grid_layout_item_size"
android:layout_gravity="top|end" tools:ignore="TouchTargetSizeCheck"
android:layout_marginTop="@dimen/grid_item_shared_icon_layout_top_margin" android:layout_marginEnd="@dimen/grid_layout_margin_end"
android:layout_marginEnd="@dimen/standard_quarter_margin"
android:clickable="true" android:clickable="true"
android:contentDescription="@string/unread_comments" android:contentDescription="@string/unread_comments"
android:focusable="true" android:focusable="true"
android:src="@drawable/ic_comment_grid" android:src="@drawable/ic_comment_grid"
android:visibility="gone" /> android:visibility="gone"
app:tint="@color/grid_file_features_icon_color"
tools:visibility="visible" />
<ImageView <ImageView
android:id="@+id/localFileIndicator" android:id="@+id/localFileIndicator"
android:layout_width="@dimen/grid_item_local_file_indicator_layout_width" android:layout_width="@dimen/grid_layout_item_size"
android:layout_height="@dimen/grid_item_local_file_indicator_layout_height" android:layout_height="@dimen/grid_layout_item_size"
android:layout_gravity="bottom|end" android:layout_marginEnd="@dimen/grid_layout_margin_end"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:layout_marginEnd="@dimen/standard_quarter_margin"
android:layout_marginBottom="@dimen/standard_quarter_margin"
android:contentDescription="@string/synced_icon" android:contentDescription="@string/synced_icon"
android:src="@drawable/ic_synced" /> android:src="@drawable/ic_synced"
tools:visibility="visible" />
<ImageView </LinearLayout>
android:id="@+id/custom_checkbox"
android:layout_width="wrap_content" <ImageView
android:layout_height="wrap_content" android:id="@+id/custom_checkbox"
android:layout_gravity="center_vertical|top" android:layout_width="@dimen/grid_checkbox_size"
android:layout_marginLeft="@dimen/standard_quarter_margin" android:layout_height="@dimen/grid_checkbox_size"
android:layout_marginRight="@dimen/standard_quarter_margin" android:layout_marginStart="@dimen/grid_checkbox_margin"
android:contentDescription="@string/checkbox" android:layout_marginTop="@dimen/grid_checkbox_margin"
android:src="@android:drawable/checkbox_off_background" /> android:contentDescription="@string/checkbox"
android:src="@android:drawable/checkbox_off_background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<TextView
android:id="@+id/Filename"
android:layout_width="@dimen/grid_filename_width"
android:layout_height="@dimen/grid_bottom_view_height"
android:layout_gravity="center"
android:layout_marginBottom="@dimen/grid_bottom_view_margin_bottom"
android:ellipsize="middle"
android:gravity="center"
android:singleLine="true"
android:text="@string/placeholder_filename"
android:textColor="@color/text_color"
android:textSize="@dimen/grid_item_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<ImageButton
android:id="@+id/more"
android:layout_width="@dimen/grid_bottom_view_height"
android:layout_height="@dimen/grid_bottom_view_height"
android:layout_marginBottom="@dimen/grid_bottom_view_margin_bottom"
android:layout_marginEnd="@dimen/grid_bottom_view_margin_end"
android:translationZ="2dp"
tools:ignore="TouchTargetSizeCheck"
android:layout_gravity="center"
android:background="@color/transparent"
android:contentDescription="@string/overflow_menu"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_dots_vertical" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<TextView </LinearLayout>
android:id="@+id/Filename"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/standard_quarter_margin"
android:layout_marginRight="@dimen/standard_quarter_margin"
android:ellipsize="middle"
android:gravity="center_horizontal"
android:singleLine="true"
android:text="@string/placeholder_filename"
android:textColor="@color/text_color"
android:textSize="@dimen/grid_item_text_size" />
</com.owncloud.android.ui.SquareLinearLayout>

View file

@ -23,6 +23,8 @@
<color name="white">#ffffff</color> <color name="white">#ffffff</color>
<color name="white_helper_text">#B3FFFFFF</color> <color name="white_helper_text">#B3FFFFFF</color>
<color name="text_color">#333333</color> <color name="text_color">#333333</color>
<color name="grid_file_features_icon_color">#303034</color>
<color name="grid_file_features_background_color">#E9E8EB</color>
<color name="drawer_text_color">@color/secondary_text_color</color> <color name="drawer_text_color">@color/secondary_text_color</color>
<color name="text_color_inverse">#ffffff</color> <color name="text_color_inverse">#ffffff</color>
<color name="disabled_text">#ff888888</color> <color name="disabled_text">#ff888888</color>

View file

@ -40,6 +40,11 @@
<dimen name="standard_double_margin">32dp</dimen> <dimen name="standard_double_margin">32dp</dimen>
<dimen name="standard_half_margin">8dp</dimen> <dimen name="standard_half_margin">8dp</dimen>
<dimen name="standard_quarter_margin">4dp</dimen> <dimen name="standard_quarter_margin">4dp</dimen>
<dimen name="grid_layout_item_size">10dp</dimen>
<dimen name="grid_layout_file_features_margin_end">24dp</dimen>
<dimen name="grid_layout_margin_end">2dp</dimen>
<dimen name="grid_layout_margin_bottom">16dp</dimen>
<dimen name="grid_sync_item_layout_next_text_size">22sp</dimen>
<dimen name="button_width">140dp</dimen> <dimen name="button_width">140dp</dimen>
<dimen name="button_extra_width">180dp</dimen> <dimen name="button_extra_width">180dp</dimen>
<dimen name="standard_eighth_margin">2dp</dimen> <dimen name="standard_eighth_margin">2dp</dimen>
@ -103,6 +108,17 @@
<dimen name="activity_row_layout_height">48dp</dimen> <dimen name="activity_row_layout_height">48dp</dimen>
<dimen name="notification_icon_width">24dp</dimen> <dimen name="notification_icon_width">24dp</dimen>
<dimen name="notification_icon_height">24dp</dimen> <dimen name="notification_icon_height">24dp</dimen>
<dimen name="grid_container_margin">4dp</dimen>
<dimen name="selected_grid_container_radius">4dp</dimen>
<dimen name="grid_checkbox_size">18dp</dimen>
<dimen name="grid_checkbox_margin">6dp</dimen>
<dimen name="grid_thumbnail_margin_bottom">18dp</dimen>
<dimen name="grid_bottom_view_margin_bottom">10dp</dimen>
<dimen name="grid_bottom_view_margin_end">6dp</dimen>
<dimen name="grid_bottom_view_height">20dp</dimen>
<dimen name="grid_container_width">130dp</dimen>
<dimen name="grid_container_height">120dp</dimen>
<dimen name="grid_filename_width">80dp</dimen>
<dimen name="notification_icon_layout_right_end_margin">21dp</dimen> <dimen name="notification_icon_layout_right_end_margin">21dp</dimen>
<dimen name="notification_list_item_grid_layout_left_start_margin">-8dp</dimen> <dimen name="notification_list_item_grid_layout_left_start_margin">-8dp</dimen>
<dimen name="uploader_list_separator_height">1dp</dimen> <dimen name="uploader_list_separator_height">1dp</dimen>
@ -110,12 +126,6 @@
<dimen name="contactlist_item_icon_layout_height">40dp</dimen> <dimen name="contactlist_item_icon_layout_height">40dp</dimen>
<dimen name="empty_list_icon_layout_width">72dp</dimen> <dimen name="empty_list_icon_layout_width">72dp</dimen>
<dimen name="empty_list_icon_layout_height">72dp</dimen> <dimen name="empty_list_icon_layout_height">72dp</dimen>
<dimen name="grid_image_icon_margin">14dp</dimen>
<dimen name="grid_image_icon_padding">14dp</dimen>
<dimen name="grid_item_shared_icon_layout_top_margin">24dp</dimen>
<dimen name="grid_item_local_file_indicator_layout_width">16dp</dimen>
<dimen name="grid_item_local_file_indicator_layout_height">16dp</dimen>
<dimen name="grid_sync_item_layout_next_text_size">22sp</dimen>
<dimen name="grid_sync_item_layout_counter_text_size">22sp</dimen> <dimen name="grid_sync_item_layout_counter_text_size">22sp</dimen>
<dimen name="list_item_favorite_action_layout_width">14dp</dimen> <dimen name="list_item_favorite_action_layout_width">14dp</dimen>
<dimen name="list_item_favorite_action_layout_height">14dp</dimen> <dimen name="list_item_favorite_action_layout_height">14dp</dimen>

View file

@ -808,6 +808,7 @@
<string name="no_map_app_availble">No App available to handle maps</string> <string name="no_map_app_availble">No App available to handle maps</string>
<string name="share_via_link_hide_download">Hide download</string> <string name="share_via_link_hide_download">Hide download</string>
<string name="unread_comments">Unread comments exist</string> <string name="unread_comments">Unread comments exist</string>
<string name="grid_file_features_live_photo_content_description">This icon indicates availability of live photo</string>
<string name="richdocuments_failed_to_load_document">Failed to load document!</string> <string name="richdocuments_failed_to_load_document">Failed to load document!</string>
<string name="create_new_document">Create new document</string> <string name="create_new_document">Create new document</string>
<string name="create_new_spreadsheet">Create new spreadsheet</string> <string name="create_new_spreadsheet">Create new spreadsheet</string>
@ -877,8 +878,9 @@
<string name="etm_background_job_name">Job name</string> <string name="etm_background_job_name">Job name</string>
<string name="etm_background_job_user">User</string> <string name="etm_background_job_user">User</string>
<string name="etm_background_job_state">State</string> <string name="etm_background_job_state">State</string>
<string name="etm_background_job_started">Started</string> <string name="etm_background_job_started">Created</string>
<string name="etm_background_job_progress">Progress</string> <string name="etm_background_job_progress">Progress</string>
<string name="etm_background_execution_count">Times run in 48h</string>
<string name="etm_migrations">Migrations (app upgrade)</string> <string name="etm_migrations">Migrations (app upgrade)</string>
<string name="etm_transfer">File transfer</string> <string name="etm_transfer">File transfer</string>
<string name="etm_transfer_remote_path">Remote path</string> <string name="etm_transfer_remote_path">Remote path</string>
@ -914,7 +916,7 @@
<string name="failed_to_start_editor">Failed to start editor</string> <string name="failed_to_start_editor">Failed to start editor</string>
<string name="create_rich_workspace">Add folder info</string> <string name="create_rich_workspace">Add folder info</string>
<string name="creates_rich_workspace">creates folder info</string> <string name="creates_rich_workspace">creates folder info</string>
<string name="uploader_local_files_uploaded">Try to upload local files again</string> <string name="uploader_local_files_uploaded">Retry to upload failed local files</string>
<string name="uploader_file_not_found_on_server_message">We couldnt locate the file on server. Another user may have deleted the file</string> <string name="uploader_file_not_found_on_server_message">We couldnt locate the file on server. Another user may have deleted the file</string>
<string name="uploader_file_not_found_message">File not found. Are you sure that this file exists or has a previous conflict not been resolved?</string> <string name="uploader_file_not_found_message">File not found. Are you sure that this file exists or has a previous conflict not been resolved?</string>
<string name="uploader_upload_failed_sync_conflict_error">File upload conflict</string> <string name="uploader_upload_failed_sync_conflict_error">File upload conflict</string>