mirror of
https://github.com/nextcloud/android.git
synced 2024-12-22 16:54:33 +03:00
Merge remote-tracking branch 'origin/master' into dev
This commit is contained in:
commit
ec228a3179
116 changed files with 3384 additions and 499 deletions
1301
app/schemas/com.nextcloud.client.database.NextcloudDatabase/84.json
Normal file
1301
app/schemas/com.nextcloud.client.database.NextcloudDatabase/84.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -17,6 +17,7 @@ import android.content.res.Configuration;
|
|||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.NetworkOnMainThreadException;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
|
||||
|
@ -375,6 +376,11 @@ public abstract class AbstractIT {
|
|||
|
||||
public void uploadOCUpload(OCUpload ocUpload) {
|
||||
ConnectivityService connectivityServiceMock = new ConnectivityService() {
|
||||
@Override
|
||||
public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return false;
|
||||
|
|
|
@ -13,6 +13,7 @@ import android.accounts.OperationCanceledException;
|
|||
import android.content.ActivityNotFoundException;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.NetworkOnMainThreadException;
|
||||
|
||||
import com.nextcloud.client.account.User;
|
||||
import com.nextcloud.client.account.UserAccountManager;
|
||||
|
@ -187,6 +188,11 @@ public abstract class AbstractOnServerIT extends AbstractIT {
|
|||
|
||||
public void uploadOCUpload(OCUpload ocUpload, int localBehaviour) {
|
||||
ConnectivityService connectivityServiceMock = new ConnectivityService() {
|
||||
@Override
|
||||
public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return false;
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
*/
|
||||
package com.owncloud.android;
|
||||
|
||||
import android.os.NetworkOnMainThreadException;
|
||||
|
||||
import com.nextcloud.client.account.UserAccountManagerImpl;
|
||||
import com.nextcloud.client.device.BatteryStatus;
|
||||
import com.nextcloud.client.device.PowerManagementService;
|
||||
|
@ -56,6 +58,11 @@ public class UploadIT extends AbstractOnServerIT {
|
|||
targetContext.getContentResolver());
|
||||
|
||||
private ConnectivityService connectivityServiceMock = new ConnectivityService() {
|
||||
@Override
|
||||
public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return false;
|
||||
|
@ -274,6 +281,11 @@ public class UploadIT extends AbstractOnServerIT {
|
|||
@Test
|
||||
public void testUploadOnWifiOnlyButNoWifi() {
|
||||
ConnectivityService connectivityServiceMock = new ConnectivityService() {
|
||||
@Override
|
||||
public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return false;
|
||||
|
@ -358,6 +370,11 @@ public class UploadIT extends AbstractOnServerIT {
|
|||
@Test
|
||||
public void testUploadOnWifiOnlyButMeteredWifi() {
|
||||
ConnectivityService connectivityServiceMock = new ConnectivityService() {
|
||||
@Override
|
||||
public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return false;
|
||||
|
|
|
@ -34,6 +34,10 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
|
|||
private var uploadsStorageManager: UploadsStorageManager? = null
|
||||
|
||||
private val connectivityServiceMock: ConnectivityService = object : ConnectivityService {
|
||||
override fun isNetworkAndServerAvailable(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun isConnected(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -62,7 +62,8 @@ public class ConflictsResolveActivityIT extends AbstractIT {
|
|||
|
||||
ConflictsResolveActivity sut = activityRule.launchActivity(intent);
|
||||
|
||||
ConflictsResolveDialog dialog = ConflictsResolveDialog.newInstance(existingFile,
|
||||
ConflictsResolveDialog dialog = ConflictsResolveDialog.newInstance(targetContext,
|
||||
existingFile,
|
||||
newFile,
|
||||
UserAccountManagerImpl
|
||||
.fromContext(targetContext)
|
||||
|
@ -209,7 +210,7 @@ public class ConflictsResolveActivityIT extends AbstractIT {
|
|||
|
||||
getInstrumentation().waitForIdleSync();
|
||||
|
||||
onView(withId(R.id.existing_checkbox)).perform(click());
|
||||
onView(withId(R.id.right_checkbox)).perform(click());
|
||||
|
||||
DialogFragment dialog = (DialogFragment) sut.getSupportFragmentManager().findFragmentByTag("conflictDialog");
|
||||
screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView());
|
||||
|
@ -255,7 +256,7 @@ public class ConflictsResolveActivityIT extends AbstractIT {
|
|||
|
||||
getInstrumentation().waitForIdleSync();
|
||||
|
||||
onView(withId(R.id.new_checkbox)).perform(click());
|
||||
onView(withId(R.id.left_checkbox)).perform(click());
|
||||
|
||||
DialogFragment dialog = (DialogFragment) sut.getSupportFragmentManager().findFragmentByTag("conflictDialog");
|
||||
screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView());
|
||||
|
@ -300,8 +301,8 @@ public class ConflictsResolveActivityIT extends AbstractIT {
|
|||
|
||||
getInstrumentation().waitForIdleSync();
|
||||
|
||||
onView(withId(R.id.existing_checkbox)).perform(click());
|
||||
onView(withId(R.id.new_checkbox)).perform(click());
|
||||
onView(withId(R.id.right_checkbox)).perform(click());
|
||||
onView(withId(R.id.left_checkbox)).perform(click());
|
||||
|
||||
DialogFragment dialog = (DialogFragment) sut.getSupportFragmentManager().findFragmentByTag("conflictDialog");
|
||||
screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView());
|
||||
|
|
|
@ -53,6 +53,10 @@ class TestActivity :
|
|||
override fun getConnectivity(): Connectivity {
|
||||
return Connectivity.CONNECTED_WIFI
|
||||
}
|
||||
|
||||
override fun isNetworkAndServerAvailable(): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
|
@ -266,6 +266,9 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name="com.nextcloud.receiver.OfflineOperationActionReceiver"
|
||||
android:exported="false" />
|
||||
<receiver
|
||||
android:name="com.nextcloud.client.jobs.MediaFoldersDetectionWork$NotificationReceiver"
|
||||
android:exported="false" />
|
||||
|
|
|
@ -11,6 +11,7 @@ import android.content.Context
|
|||
import com.nextcloud.client.core.Clock
|
||||
import com.nextcloud.client.database.dao.ArbitraryDataDao
|
||||
import com.nextcloud.client.database.dao.FileDao
|
||||
import com.nextcloud.client.database.dao.OfflineOperationDao
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
@ -33,4 +34,9 @@ class DatabaseModule {
|
|||
fun fileDao(nextcloudDatabase: NextcloudDatabase): FileDao {
|
||||
return nextcloudDatabase.fileDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun offlineOperationsDao(nextcloudDatabase: NextcloudDatabase): OfflineOperationDao {
|
||||
return nextcloudDatabase.offlineOperationDao()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,11 +16,13 @@ import com.nextcloud.client.core.Clock
|
|||
import com.nextcloud.client.core.ClockImpl
|
||||
import com.nextcloud.client.database.dao.ArbitraryDataDao
|
||||
import com.nextcloud.client.database.dao.FileDao
|
||||
import com.nextcloud.client.database.dao.OfflineOperationDao
|
||||
import com.nextcloud.client.database.entity.ArbitraryDataEntity
|
||||
import com.nextcloud.client.database.entity.CapabilityEntity
|
||||
import com.nextcloud.client.database.entity.ExternalLinkEntity
|
||||
import com.nextcloud.client.database.entity.FileEntity
|
||||
import com.nextcloud.client.database.entity.FilesystemEntity
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
import com.nextcloud.client.database.entity.ShareEntity
|
||||
import com.nextcloud.client.database.entity.SyncedFolderEntity
|
||||
import com.nextcloud.client.database.entity.UploadEntity
|
||||
|
@ -41,7 +43,8 @@ import com.owncloud.android.db.ProviderMeta
|
|||
ShareEntity::class,
|
||||
SyncedFolderEntity::class,
|
||||
UploadEntity::class,
|
||||
VirtualEntity::class
|
||||
VirtualEntity::class,
|
||||
OfflineOperationEntity::class
|
||||
],
|
||||
version = ProviderMeta.DB_VERSION,
|
||||
autoMigrations = [
|
||||
|
@ -61,7 +64,8 @@ import com.owncloud.android.db.ProviderMeta
|
|||
AutoMigration(from = 79, to = 80),
|
||||
AutoMigration(from = 80, to = 81),
|
||||
AutoMigration(from = 81, to = 82),
|
||||
AutoMigration(from = 82, to = 83)
|
||||
AutoMigration(from = 82, to = 83),
|
||||
AutoMigration(from = 83, to = 84)
|
||||
],
|
||||
exportSchema = true
|
||||
)
|
||||
|
@ -70,6 +74,7 @@ abstract class NextcloudDatabase : RoomDatabase() {
|
|||
|
||||
abstract fun arbitraryDataDao(): ArbitraryDataDao
|
||||
abstract fun fileDao(): FileDao
|
||||
abstract fun offlineOperationDao(): OfflineOperationDao
|
||||
|
||||
companion object {
|
||||
const val FIRST_ROOM_DB_VERSION = 65
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
|
||||
@Dao
|
||||
interface OfflineOperationDao {
|
||||
@Query("SELECT * FROM offline_operations")
|
||||
fun getAll(): List<OfflineOperationEntity>
|
||||
|
||||
@Insert
|
||||
fun insert(vararg entity: OfflineOperationEntity)
|
||||
|
||||
@Update
|
||||
fun update(entity: OfflineOperationEntity)
|
||||
|
||||
@Delete
|
||||
fun delete(entity: OfflineOperationEntity)
|
||||
|
||||
@Query("DELETE FROM offline_operations WHERE offline_operations_path = :path")
|
||||
fun deleteByPath(path: String)
|
||||
|
||||
@Query("SELECT * FROM offline_operations WHERE offline_operations_path = :path LIMIT 1")
|
||||
fun getByPath(path: String): OfflineOperationEntity?
|
||||
|
||||
@Query("SELECT * FROM offline_operations WHERE offline_operations_parent_oc_file_id = :parentOCFileId")
|
||||
fun getSubDirectoriesByParentOCFileId(parentOCFileId: Long): List<OfflineOperationEntity>
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.nextcloud.model.OfflineOperationType
|
||||
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
|
||||
|
||||
@Entity(tableName = ProviderTableMeta.OFFLINE_OPERATION_TABLE_NAME)
|
||||
data class OfflineOperationEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = ProviderTableMeta._ID)
|
||||
val id: Int? = null,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PARENT_OC_FILE_ID)
|
||||
var parentOCFileId: Long? = null,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PARENT_PATH)
|
||||
var parentPath: String? = null,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_TYPE)
|
||||
var type: OfflineOperationType? = null,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PATH)
|
||||
var path: String? = null,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_FILE_NAME)
|
||||
var filename: String? = null,
|
||||
|
||||
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_CREATED_AT)
|
||||
var createdAt: Long? = null
|
||||
)
|
|
@ -28,6 +28,7 @@ import com.nextcloud.client.core.ClockImpl;
|
|||
import com.nextcloud.client.core.ThreadPoolAsyncRunner;
|
||||
import com.nextcloud.client.database.dao.ArbitraryDataDao;
|
||||
import com.nextcloud.client.device.DeviceInfo;
|
||||
import com.nextcloud.client.jobs.operation.FileOperationHelper;
|
||||
import com.nextcloud.client.logger.FileLogHandler;
|
||||
import com.nextcloud.client.logger.Logger;
|
||||
import com.nextcloud.client.logger.LoggerImpl;
|
||||
|
@ -250,6 +251,11 @@ class AppModule {
|
|||
return new PassCodeManager(preferences, clock);
|
||||
}
|
||||
|
||||
@Provides
|
||||
FileOperationHelper fileOperationHelper(CurrentAccountProvider currentAccountProvider, Context context) {
|
||||
return new FileOperationHelper(currentAccountProvider.getUser(), context, fileDataStorageManager(currentAccountProvider, context));
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
UsersAndGroupsSearchConfig userAndGroupSearchConfig() {
|
||||
|
|
|
@ -24,6 +24,7 @@ import com.nextcloud.client.onboarding.WhatsNewActivity;
|
|||
import com.nextcloud.client.widget.DashboardWidgetConfigurationActivity;
|
||||
import com.nextcloud.client.widget.DashboardWidgetProvider;
|
||||
import com.nextcloud.client.widget.DashboardWidgetService;
|
||||
import com.nextcloud.receiver.NetworkChangeReceiver;
|
||||
import com.nextcloud.ui.ChooseAccountDialogFragment;
|
||||
import com.nextcloud.ui.ImageDetailFragment;
|
||||
import com.nextcloud.ui.SetStatusDialogFragment;
|
||||
|
@ -313,6 +314,9 @@ abstract class ComponentsModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract BootupBroadcastReceiver bootupBroadcastReceiver();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract NetworkChangeReceiver networkChangeReceiver();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract NotificationWork.NotificationReceiver notificationWorkBroadcastReceiver();
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import com.nextcloud.client.documentscan.GeneratePDFUseCase
|
|||
import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
|
||||
import com.nextcloud.client.integrations.deck.DeckApi
|
||||
import com.nextcloud.client.jobs.download.FileDownloadWorker
|
||||
import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker
|
||||
import com.nextcloud.client.jobs.upload.FileUploadWorker
|
||||
import com.nextcloud.client.logger.Logger
|
||||
import com.nextcloud.client.network.ConnectivityService
|
||||
|
@ -95,12 +96,17 @@ class BackgroundJobFactory @Inject constructor(
|
|||
GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters)
|
||||
HealthStatusWork::class -> createHealthStatusWork(context, workerParameters)
|
||||
TestJob::class -> createTestJob(context, workerParameters)
|
||||
OfflineOperationsWorker::class -> createOfflineOperationsWorker(context, workerParameters)
|
||||
InternalTwoWaySyncWork::class -> createInternalTwoWaySyncWork(context, workerParameters)
|
||||
else -> null // caller falls back to default factory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createOfflineOperationsWorker(context: Context, params: WorkerParameters): ListenableWorker {
|
||||
return OfflineOperationsWorker(accountManager.user, context, connectivityService, viewThemeUtils.get(), params)
|
||||
}
|
||||
|
||||
private fun createFilesExportWork(context: Context, params: WorkerParameters): ListenableWorker {
|
||||
return FilesExportWork(
|
||||
context,
|
||||
|
|
|
@ -168,5 +168,7 @@ interface BackgroundJobManager {
|
|||
fun schedulePeriodicHealthStatus()
|
||||
fun startHealthStatus()
|
||||
fun bothFilesSyncJobsRunning(syncedFolderID: Long): Boolean
|
||||
fun startOfflineOperations()
|
||||
fun startPeriodicallyOfflineOperation()
|
||||
fun scheduleInternal2WaySync()
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import com.nextcloud.client.core.Clock
|
|||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
|
||||
import com.nextcloud.client.jobs.download.FileDownloadWorker
|
||||
import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker
|
||||
import com.nextcloud.client.jobs.upload.FileUploadWorker
|
||||
import com.nextcloud.client.preferences.AppPreferences
|
||||
import com.nextcloud.utils.extensions.isWorkRunning
|
||||
|
@ -80,7 +81,8 @@ internal class BackgroundJobManagerImpl(
|
|||
const val JOB_PDF_GENERATION = "pdf_generation"
|
||||
const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup"
|
||||
const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export"
|
||||
|
||||
const val JOB_OFFLINE_OPERATIONS = "offline_operations"
|
||||
const val JOB_PERIODIC_OFFLINE_OPERATIONS = "periodic_offline_operations"
|
||||
const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status"
|
||||
const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status"
|
||||
|
||||
|
@ -98,6 +100,7 @@ internal class BackgroundJobManagerImpl(
|
|||
const val NOT_SET_VALUE = "not set"
|
||||
const val PERIODIC_BACKUP_INTERVAL_MINUTES = 24 * 60L
|
||||
const val DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES = 15L
|
||||
const val OFFLINE_OPERATIONS_PERIODIC_JOB_INTERVAL_MINUTES = 5L
|
||||
const val DEFAULT_IMMEDIATE_JOB_DELAY_SEC = 3L
|
||||
|
||||
private const val KEEP_LOG_MILLIS = 1000 * 60 * 60 * 24 * 3L
|
||||
|
@ -198,13 +201,15 @@ internal class BackgroundJobManagerImpl(
|
|||
private fun oneTimeRequestBuilder(
|
||||
jobClass: KClass<out ListenableWorker>,
|
||||
jobName: String,
|
||||
user: User? = null
|
||||
user: User? = null,
|
||||
constraints: Constraints = Constraints.Builder().build()
|
||||
): OneTimeWorkRequest.Builder {
|
||||
val builder = OneTimeWorkRequest.Builder(jobClass.java)
|
||||
.addTag(TAG_ALL)
|
||||
.addTag(formatNameTag(jobName, user))
|
||||
.addTag(formatTimeTag(clock.currentTime))
|
||||
.addTag(formatClassTag(jobClass))
|
||||
.setConstraints(constraints)
|
||||
user?.let { builder.addTag(formatUserTag(it)) }
|
||||
return builder
|
||||
}
|
||||
|
@ -217,7 +222,8 @@ internal class BackgroundJobManagerImpl(
|
|||
jobName: String,
|
||||
intervalMins: Long = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES,
|
||||
flexIntervalMins: Long = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES,
|
||||
user: User? = null
|
||||
user: User? = null,
|
||||
constraints: Constraints = Constraints.Builder().build()
|
||||
): PeriodicWorkRequest.Builder {
|
||||
val builder = PeriodicWorkRequest.Builder(
|
||||
jobClass.java,
|
||||
|
@ -230,6 +236,7 @@ internal class BackgroundJobManagerImpl(
|
|||
.addTag(formatNameTag(jobName, user))
|
||||
.addTag(formatTimeTag(clock.currentTime))
|
||||
.addTag(formatClassTag(jobClass))
|
||||
.setConstraints(constraints)
|
||||
user?.let { builder.addTag(formatUserTag(it)) }
|
||||
return builder
|
||||
}
|
||||
|
@ -411,6 +418,47 @@ internal class BackgroundJobManagerImpl(
|
|||
workManager.isWorkRunning(JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID)
|
||||
}
|
||||
|
||||
override fun startPeriodicallyOfflineOperation() {
|
||||
val inputData = Data.Builder()
|
||||
.putString(OfflineOperationsWorker.JOB_NAME, JOB_PERIODIC_OFFLINE_OPERATIONS)
|
||||
.build()
|
||||
|
||||
val request = periodicRequestBuilder(
|
||||
jobClass = OfflineOperationsWorker::class,
|
||||
jobName = JOB_PERIODIC_OFFLINE_OPERATIONS,
|
||||
intervalMins = OFFLINE_OPERATIONS_PERIODIC_JOB_INTERVAL_MINUTES
|
||||
)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
JOB_PERIODIC_OFFLINE_OPERATIONS,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun startOfflineOperations() {
|
||||
val inputData = Data.Builder()
|
||||
.putString(OfflineOperationsWorker.JOB_NAME, JOB_OFFLINE_OPERATIONS)
|
||||
.build()
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
val request =
|
||||
oneTimeRequestBuilder(OfflineOperationsWorker::class, JOB_OFFLINE_OPERATIONS, constraints = constraints)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(
|
||||
JOB_OFFLINE_OPERATIONS,
|
||||
ExistingWorkPolicy.REPLACE,
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun schedulePeriodicFilesSyncJob(syncedFolderID: Long) {
|
||||
val arguments = Data.Builder()
|
||||
.putLong(FilesSyncWork.SYNCED_FOLDER_ID, syncedFolderID)
|
||||
|
|
|
@ -30,12 +30,16 @@ open class WorkerNotificationManager(
|
|||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
var notificationBuilder: NotificationCompat.Builder =
|
||||
NotificationUtils.newNotificationBuilder(context, "WorkerNotificationManager", viewThemeUtils).apply {
|
||||
NotificationUtils.newNotificationBuilder(
|
||||
context,
|
||||
NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS,
|
||||
viewThemeUtils
|
||||
).apply {
|
||||
setTicker(context.getString(tickerId))
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon))
|
||||
setStyle(NotificationCompat.BigTextStyle())
|
||||
setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD)
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.jobs.offlineOperations
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
import com.nextcloud.client.jobs.notification.WorkerNotificationManager
|
||||
import com.nextcloud.receiver.OfflineOperationActionReceiver
|
||||
import com.nextcloud.utils.extensions.getErrorMessage
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
import com.owncloud.android.ui.activity.ConflictsResolveActivity
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
|
||||
class OfflineOperationsNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils) :
|
||||
WorkerNotificationManager(
|
||||
ID,
|
||||
context,
|
||||
viewThemeUtils,
|
||||
R.string.offline_operations_worker_notification_manager_ticker
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val ID = 121
|
||||
private const val ERROR_ID = 122
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun start() {
|
||||
notificationBuilder.run {
|
||||
setContentTitle(context.getString(R.string.offline_operations_worker_notification_start_text))
|
||||
setProgress(100, 0, false)
|
||||
}
|
||||
|
||||
showNotification()
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun update(totalOperationSize: Int, currentOperationIndex: Int, filename: String) {
|
||||
val title = if (totalOperationSize > 1) {
|
||||
String.format(
|
||||
context.getString(R.string.offline_operations_worker_progress_text),
|
||||
currentOperationIndex,
|
||||
totalOperationSize,
|
||||
filename
|
||||
)
|
||||
} else {
|
||||
filename
|
||||
}
|
||||
|
||||
val progress = (currentOperationIndex * 100) / totalOperationSize
|
||||
|
||||
notificationBuilder.run {
|
||||
setContentTitle(title)
|
||||
setProgress(100, progress, false)
|
||||
}
|
||||
|
||||
showNotification()
|
||||
}
|
||||
|
||||
fun showNewNotification(result: RemoteOperationResult<*>, operation: RemoteOperation<*>) {
|
||||
val reason = (result to operation).getErrorMessage()
|
||||
val text = context.getString(R.string.offline_operations_worker_notification_error_text, reason)
|
||||
|
||||
notificationBuilder.run {
|
||||
setContentTitle(text)
|
||||
setOngoing(false)
|
||||
notificationManager.notify(ERROR_ID, this.build())
|
||||
}
|
||||
}
|
||||
|
||||
fun showConflictResolveNotification(file: OCFile, entity: OfflineOperationEntity?, user: User) {
|
||||
val path = entity?.path
|
||||
val id = entity?.id
|
||||
|
||||
if (path == null || id == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val resolveConflictIntent = ConflictsResolveActivity.createIntent(file, path, context)
|
||||
val resolveConflictPendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
id,
|
||||
resolveConflictIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
val resolveConflictAction = NotificationCompat.Action(
|
||||
R.drawable.ic_cloud_upload,
|
||||
context.getString(R.string.upload_list_resolve_conflict),
|
||||
resolveConflictPendingIntent
|
||||
)
|
||||
|
||||
val deleteIntent = Intent(context, OfflineOperationActionReceiver::class.java).apply {
|
||||
putExtra(OfflineOperationActionReceiver.FILE_PATH, path)
|
||||
putExtra(OfflineOperationActionReceiver.USER, user)
|
||||
}
|
||||
val deletePendingIntent =
|
||||
PendingIntent.getBroadcast(context, 0, deleteIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||
val deleteAction = NotificationCompat.Action(
|
||||
R.drawable.ic_delete,
|
||||
context.getString(R.string.offline_operations_worker_notification_delete_offline_folder),
|
||||
deletePendingIntent
|
||||
)
|
||||
|
||||
val title = context.getString(
|
||||
R.string.offline_operations_worker_notification_conflict_text,
|
||||
file.fileName
|
||||
)
|
||||
|
||||
notificationBuilder
|
||||
.clearActions()
|
||||
.setContentTitle(title)
|
||||
.setContentIntent(resolveConflictPendingIntent)
|
||||
.addAction(deleteAction)
|
||||
.addAction(resolveConflictAction)
|
||||
|
||||
notificationManager.notify(id, notificationBuilder.build())
|
||||
}
|
||||
|
||||
fun dismissNotification(id: Int?) {
|
||||
if (id == null) return
|
||||
notificationManager.cancel(id)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.jobs.offlineOperations
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepository
|
||||
import com.nextcloud.client.network.ClientFactoryImpl
|
||||
import com.nextcloud.client.network.ConnectivityService
|
||||
import com.nextcloud.model.OfflineOperationType
|
||||
import com.nextcloud.model.WorkerState
|
||||
import com.nextcloud.model.WorkerStateLiveData
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.lib.common.OwnCloudClient
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.operations.CreateFolderOperation
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class OfflineOperationsWorker(
|
||||
private val user: User,
|
||||
private val context: Context,
|
||||
private val connectivityService: ConnectivityService,
|
||||
viewThemeUtils: ViewThemeUtils,
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
companion object {
|
||||
private val TAG = OfflineOperationsWorker::class.java.simpleName
|
||||
const val JOB_NAME = "JOB_NAME"
|
||||
}
|
||||
|
||||
private val fileDataStorageManager = FileDataStorageManager(user, context.contentResolver)
|
||||
private val clientFactory = ClientFactoryImpl(context)
|
||||
private val notificationManager = OfflineOperationsNotificationManager(context, viewThemeUtils)
|
||||
private var repository = OfflineOperationsRepository(fileDataStorageManager)
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override suspend fun doWork(): Result = coroutineScope {
|
||||
val jobName = inputData.getString(JOB_NAME)
|
||||
Log_OC.d(
|
||||
TAG,
|
||||
"$jobName -----------------------------------\n" +
|
||||
"OfflineOperationsWorker started" +
|
||||
"\n-----------------------------------"
|
||||
)
|
||||
|
||||
if (!connectivityService.isNetworkAndServerAvailable()) {
|
||||
Log_OC.d(TAG, "OfflineOperationsWorker cancelled, no internet connection")
|
||||
return@coroutineScope Result.retry()
|
||||
}
|
||||
|
||||
val client = clientFactory.create(user)
|
||||
notificationManager.start()
|
||||
|
||||
var operations = fileDataStorageManager.offlineOperationDao.getAll()
|
||||
val totalOperations = operations.size
|
||||
var currentSuccessfulOperationIndex = 0
|
||||
|
||||
return@coroutineScope try {
|
||||
while (operations.isNotEmpty()) {
|
||||
val operation = operations.first()
|
||||
val result = executeOperation(operation, client)
|
||||
val isSuccess = handleResult(
|
||||
operation,
|
||||
totalOperations,
|
||||
currentSuccessfulOperationIndex,
|
||||
result?.first,
|
||||
result?.second
|
||||
)
|
||||
|
||||
operations = if (isSuccess) {
|
||||
currentSuccessfulOperationIndex++
|
||||
fileDataStorageManager.offlineOperationDao.getAll()
|
||||
} else {
|
||||
operations.filter { it != operation }
|
||||
}
|
||||
}
|
||||
|
||||
Log_OC.d(TAG, "OfflineOperationsWorker successfully completed")
|
||||
notificationManager.dismissNotification()
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.OfflineOperationsCompleted)
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Log_OC.d(TAG, "OfflineOperationsWorker terminated: $e")
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("Deprecation")
|
||||
private suspend fun executeOperation(
|
||||
operation: OfflineOperationEntity,
|
||||
client: OwnCloudClient
|
||||
): Pair<RemoteOperationResult<*>?, RemoteOperation<*>?>? {
|
||||
return when (operation.type) {
|
||||
OfflineOperationType.CreateFolder -> {
|
||||
if (operation.parentPath != null) {
|
||||
val createFolderOperation = withContext(Dispatchers.IO) {
|
||||
CreateFolderOperation(
|
||||
operation.path,
|
||||
user,
|
||||
context,
|
||||
fileDataStorageManager
|
||||
)
|
||||
}
|
||||
createFolderOperation.execute(client) to createFolderOperation
|
||||
} else {
|
||||
Log_OC.d(TAG, "CreateFolder operation incomplete, missing parentPath")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log_OC.d(TAG, "Unsupported operation type: ${operation.type}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleResult(
|
||||
operation: OfflineOperationEntity,
|
||||
totalOperations: Int,
|
||||
currentSuccessfulOperationIndex: Int,
|
||||
result: RemoteOperationResult<*>?,
|
||||
remoteOperation: RemoteOperation<*>?
|
||||
): Boolean {
|
||||
if (result == null) {
|
||||
Log_OC.d(TAG, "Operation not completed, result is null")
|
||||
return false
|
||||
}
|
||||
|
||||
val logMessage = if (result.isSuccess) "Operation completed" else "Operation failed"
|
||||
Log_OC.d(TAG, "$logMessage path: ${operation.path}, type: ${operation.type}")
|
||||
|
||||
if (result.isSuccess) {
|
||||
repository.updateNextOperations(operation)
|
||||
fileDataStorageManager.offlineOperationDao.delete(operation)
|
||||
notificationManager.update(totalOperations, currentSuccessfulOperationIndex, operation.filename ?: "")
|
||||
} else {
|
||||
val excludedErrorCodes = listOf(RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS)
|
||||
|
||||
if (remoteOperation != null && !excludedErrorCodes.contains(result.code)) {
|
||||
notificationManager.showNewNotification(result, remoteOperation)
|
||||
}
|
||||
}
|
||||
|
||||
return result.isSuccess
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.jobs.offlineOperations.repository
|
||||
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
|
||||
class OfflineOperationsRepository(
|
||||
private val fileDataStorageManager: FileDataStorageManager
|
||||
) : OfflineOperationsRepositoryType {
|
||||
|
||||
private val dao = fileDataStorageManager.offlineOperationDao
|
||||
private val pathSeparator = '/'
|
||||
|
||||
@Suppress("NestedBlockDepth")
|
||||
override fun getAllSubdirectories(fileId: Long): List<OfflineOperationEntity> {
|
||||
val result = mutableListOf<OfflineOperationEntity>()
|
||||
val queue = ArrayDeque<Long>()
|
||||
queue.add(fileId)
|
||||
val processedIds = mutableSetOf<Long>()
|
||||
|
||||
while (queue.isNotEmpty()) {
|
||||
val currentFileId = queue.removeFirst()
|
||||
if (currentFileId in processedIds || currentFileId == 1L) continue
|
||||
|
||||
processedIds.add(currentFileId)
|
||||
|
||||
val subDirectories = dao.getSubDirectoriesByParentOCFileId(currentFileId)
|
||||
result.addAll(subDirectories)
|
||||
|
||||
subDirectories.forEach {
|
||||
val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(it.path)
|
||||
ocFile?.fileId?.let { newFileId ->
|
||||
if (newFileId != 1L && newFileId !in processedIds) {
|
||||
queue.add(newFileId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
override fun deleteOperation(file: OCFile) {
|
||||
getAllSubdirectories(file.fileId).forEach {
|
||||
dao.delete(it)
|
||||
}
|
||||
|
||||
file.decryptedRemotePath?.let {
|
||||
val entity = dao.getByPath(it)
|
||||
entity?.let {
|
||||
dao.delete(entity)
|
||||
}
|
||||
}
|
||||
|
||||
fileDataStorageManager.removeFile(file, true, true)
|
||||
}
|
||||
|
||||
override fun updateNextOperations(operation: OfflineOperationEntity) {
|
||||
val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(operation.path)
|
||||
val fileId = ocFile?.fileId ?: return
|
||||
|
||||
getAllSubdirectories(fileId)
|
||||
.mapNotNull { nextOperation ->
|
||||
nextOperation.parentOCFileId?.let { parentId ->
|
||||
fileDataStorageManager.getFileById(parentId)?.let { ocFile ->
|
||||
ocFile.decryptedRemotePath?.let { updatedPath ->
|
||||
val newParentPath = ocFile.parentRemotePath
|
||||
val newPath = updatedPath + nextOperation.filename + pathSeparator
|
||||
|
||||
if (newParentPath != nextOperation.parentPath || newPath != nextOperation.path) {
|
||||
nextOperation.apply {
|
||||
parentPath = newParentPath
|
||||
path = newPath
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.forEach { dao.update(it) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.jobs.offlineOperations.repository
|
||||
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
|
||||
interface OfflineOperationsRepositoryType {
|
||||
fun getAllSubdirectories(fileId: Long): List<OfflineOperationEntity>
|
||||
fun deleteOperation(file: OCFile)
|
||||
fun updateNextOperations(operation: OfflineOperationEntity)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.jobs.operation
|
||||
|
||||
import android.content.Context
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.network.ClientFactoryImpl
|
||||
import com.nextcloud.utils.extensions.getErrorMessage
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.operations.RemoveFileOperation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FileOperationHelper(
|
||||
private val user: User,
|
||||
private val context: Context,
|
||||
private val fileDataStorageManager: FileDataStorageManager
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val TAG = FileOperationHelper::class.java.simpleName
|
||||
}
|
||||
|
||||
private val clientFactory = ClientFactoryImpl(context)
|
||||
private val client = clientFactory.create(user)
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "Deprecation")
|
||||
suspend fun removeFile(file: OCFile, onlyLocalCopy: Boolean, inBackground: Boolean): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val operation = async {
|
||||
RemoveFileOperation(
|
||||
file,
|
||||
onlyLocalCopy,
|
||||
user,
|
||||
inBackground,
|
||||
context,
|
||||
fileDataStorageManager
|
||||
)
|
||||
}
|
||||
val operationResult = operation.await()
|
||||
val result = operationResult.execute(client)
|
||||
|
||||
return@withContext if (result.isSuccess) {
|
||||
true
|
||||
} else {
|
||||
val reason = (result to operationResult).getErrorMessage()
|
||||
Log_OC.e(TAG, "Error occurred while removing file: $reason")
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "Error occurred while removing file: $e")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,11 +6,28 @@
|
|||
*/
|
||||
package com.nextcloud.client.network;
|
||||
|
||||
import android.os.NetworkOnMainThreadException;
|
||||
|
||||
/**
|
||||
* This service provides information about current network connectivity
|
||||
* and server reachability.
|
||||
*/
|
||||
public interface ConnectivityService {
|
||||
/**
|
||||
* Checks the availability of the server and the device's internet connection.
|
||||
* <p>
|
||||
* This method performs a network request to verify if the server is accessible and
|
||||
* checks if the device has an active internet connection. Due to the network operations involved,
|
||||
* this method should be executed on a background thread to avoid blocking the main thread.
|
||||
* </p>
|
||||
*
|
||||
* @return {@code true} if the server is accessible and the device has an internet connection;
|
||||
* {@code false} otherwise.
|
||||
*
|
||||
* @throws NetworkOnMainThreadException if this function runs on main thread.
|
||||
*/
|
||||
boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException;
|
||||
|
||||
boolean isConnected();
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,6 +13,7 @@ import android.net.ConnectivityManager;
|
|||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.NetworkOnMainThreadException;
|
||||
|
||||
import com.nextcloud.client.account.Server;
|
||||
import com.nextcloud.client.account.UserAccountManager;
|
||||
|
@ -55,6 +56,19 @@ class ConnectivityServiceImpl implements ConnectivityService {
|
|||
this.walledCheckCache = walledCheckCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
|
||||
Network activeNetwork = platformConnectivityManager.getActiveNetwork();
|
||||
NetworkCapabilities networkCapabilities = platformConnectivityManager.getNetworkCapabilities(activeNetwork);
|
||||
boolean hasInternet = networkCapabilities != null && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
|
||||
|
||||
if (!hasInternet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !isInternetWalled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
Network nw = platformConnectivityManager.getActiveNetwork();
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.model
|
||||
|
||||
enum class OfflineOperationType {
|
||||
CreateFolder
|
||||
}
|
|
@ -17,4 +17,5 @@ sealed class WorkerState {
|
|||
data class DownloadStarted(var user: User?, var currentDownload: DownloadFileOperation?) : WorkerState()
|
||||
data class UploadFinished(var currentFile: OCFile?) : WorkerState()
|
||||
data class UploadStarted(var user: User?, var uploads: List<OCUpload>) : WorkerState()
|
||||
data object OfflineOperationsCompleted : WorkerState()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.nextcloud.client.network.ConnectivityService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
interface NetworkChangeListener {
|
||||
fun networkAndServerConnectionListener(isNetworkAndServerAvailable: Boolean)
|
||||
}
|
||||
|
||||
class NetworkChangeReceiver(
|
||||
private val listener: NetworkChangeListener,
|
||||
private val connectivityService: ConnectivityService
|
||||
) : BroadcastReceiver() {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
scope.launch {
|
||||
val isNetworkAndServerAvailable = connectivityService.isNetworkAndServerAvailable()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
listener.networkAndServerConnectionListener(isNetworkAndServerAvailable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.utils.extensions.getParcelableArgument
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
|
||||
class OfflineOperationActionReceiver : BroadcastReceiver() {
|
||||
companion object {
|
||||
const val FILE_PATH = "FILE_PATH"
|
||||
const val USER = "USER"
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val path = intent?.getStringExtra(FILE_PATH) ?: return
|
||||
val user = intent.getParcelableArgument(USER, User::class.java) ?: return
|
||||
val fileDataStorageManager = FileDataStorageManager(user, context?.contentResolver)
|
||||
fileDataStorageManager.offlineOperationDao.deleteByPath(path)
|
||||
// TODO Update notification
|
||||
}
|
||||
}
|
|
@ -52,7 +52,10 @@ enum class FileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRe
|
|||
LOCK_FILE(R.id.action_lock_file, R.string.lock_file, R.drawable.ic_lock),
|
||||
|
||||
// Shortcuts
|
||||
PIN_TO_HOMESCREEN(R.id.action_pin_to_homescreen, R.string.pin_home, R.drawable.add_to_home_screen);
|
||||
PIN_TO_HOMESCREEN(R.id.action_pin_to_homescreen, R.string.pin_home, R.drawable.add_to_home_screen),
|
||||
|
||||
// Retry for offline operation
|
||||
RETRY(R.id.action_retry, R.string.retry, R.drawable.ic_retry);
|
||||
|
||||
companion object {
|
||||
/**
|
||||
|
@ -82,7 +85,8 @@ enum class FileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRe
|
|||
UNSET_ENCRYPTED,
|
||||
SET_AS_WALLPAPER,
|
||||
REMOVE_FILE,
|
||||
PIN_TO_HOMESCREEN
|
||||
PIN_TO_HOMESCREEN,
|
||||
RETRY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.utils.date
|
||||
|
||||
enum class DateFormatPattern(val pattern: String) {
|
||||
/**
|
||||
* e.g. 10.11.2024 - 12:44
|
||||
*/
|
||||
FullDateWithHours("dd.MM.yyyy - HH:mm")
|
||||
}
|
|
@ -10,6 +10,10 @@ package com.nextcloud.utils.extensions
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
fun AppCompatActivity.isDialogFragmentReady(fragment: Fragment): Boolean = isActive() && !fragment.isStateSaved()
|
||||
fun AppCompatActivity.isDialogFragmentReady(fragment: Fragment): Boolean = isActive() && !fragment.isStateSaved
|
||||
|
||||
fun AppCompatActivity.isActive(): Boolean = !isFinishing && !isDestroyed
|
||||
|
||||
fun AppCompatActivity.fragments(): List<Fragment> = supportFragmentManager.fragments
|
||||
|
||||
fun AppCompatActivity.lastFragment(): Fragment = fragments().last()
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.utils.extensions
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.nextcloud.utils.date.DateFormatPattern
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
fun Date.currentDateRepresentation(formatPattern: DateFormatPattern): String {
|
||||
return SimpleDateFormat(formatPattern.pattern).format(this)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.utils.extensions
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
inline fun <reified T : Parcelable> Parcel.readParcelableCompat(classLoader: ClassLoader?): T? {
|
||||
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
|
||||
readParcelable(classLoader, T::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
readParcelable(classLoader)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.utils.extensions
|
||||
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
import com.owncloud.android.MainApp
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
import com.owncloud.android.lib.resources.files.model.RemoteFile
|
||||
import com.owncloud.android.utils.ErrorMessageAdapter
|
||||
import com.owncloud.android.utils.FileStorageUtils
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
fun Pair<RemoteOperationResult<*>?, RemoteOperation<*>?>?.getErrorMessage(): String {
|
||||
val result = this?.first ?: return MainApp.string(R.string.unexpected_error_occurred)
|
||||
val operation = this.second ?: return MainApp.string(R.string.unexpected_error_occurred)
|
||||
return ErrorMessageAdapter.getErrorCauseMessage(result, operation, MainApp.getAppContext().resources)
|
||||
}
|
||||
|
||||
@Suppress("NestedBlockDepth")
|
||||
fun RemoteOperationResult<*>?.getConflictedRemoteIdsWithOfflineOperations(
|
||||
offlineOperations: List<OfflineOperationEntity>,
|
||||
fileDataStorageManager: FileDataStorageManager
|
||||
): HashMap<String, String>? {
|
||||
val newFiles = toOCFile() ?: return null
|
||||
val result = hashMapOf<String, String>()
|
||||
|
||||
offlineOperations.forEach { operation ->
|
||||
newFiles.forEach { file ->
|
||||
if (fileDataStorageManager.fileExists(operation.path) && operation.filename == file.fileName) {
|
||||
operation.path?.let { path ->
|
||||
result[file.remoteId] = path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.ifEmpty { null }
|
||||
}
|
||||
|
||||
@Suppress("Deprecation")
|
||||
fun RemoteOperationResult<*>?.toOCFile(): List<OCFile>? {
|
||||
return if (this?.isSuccess == true) {
|
||||
data?.toOCFileList()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun ArrayList<Any>.toOCFileList(): List<OCFile> {
|
||||
return this.mapNotNull {
|
||||
val remoteFile = (it as? RemoteFile)
|
||||
|
||||
remoteFile?.let {
|
||||
remoteFile.toOCFile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun RemoteFile?.toOCFile(): OCFile = FileStorageUtils.fillOCFile(this)
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
|
@ -13,6 +13,11 @@ import android.util.TypedValue
|
|||
import android.view.View
|
||||
import android.view.ViewOutlineProvider
|
||||
|
||||
fun View?.setVisibleIf(condition: Boolean) {
|
||||
if (this == null) return
|
||||
visibility = if (condition) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
fun createRoundedOutline(context: Context, cornerRadiusValue: Float): ViewOutlineProvider {
|
||||
return object : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
|
|
|
@ -31,6 +31,7 @@ import android.content.pm.ActivityInfo;
|
|||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
|
@ -59,6 +60,8 @@ import com.nextcloud.client.onboarding.OnboardingService;
|
|||
import com.nextcloud.client.preferences.AppPreferences;
|
||||
import com.nextcloud.client.preferences.AppPreferencesImpl;
|
||||
import com.nextcloud.client.preferences.DarkMode;
|
||||
import com.nextcloud.receiver.NetworkChangeListener;
|
||||
import com.nextcloud.receiver.NetworkChangeReceiver;
|
||||
import com.nextcloud.utils.extensions.ContextExtensionsKt;
|
||||
import com.nmc.android.ui.LauncherActivity;
|
||||
import com.owncloud.android.authentication.AuthenticatorActivity;
|
||||
|
@ -129,7 +132,7 @@ import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFER
|
|||
* Main Application of the project.
|
||||
* Contains methods to build the "static" strings. These strings were before constants in different classes.
|
||||
*/
|
||||
public class MainApp extends Application implements HasAndroidInjector {
|
||||
public class MainApp extends Application implements HasAndroidInjector, NetworkChangeListener {
|
||||
public static final OwnCloudVersion OUTDATED_SERVER_VERSION = NextcloudVersion.nextcloud_26;
|
||||
public static final OwnCloudVersion MINIMUM_SUPPORTED_SERVER_VERSION = OwnCloudVersion.nextcloud_17;
|
||||
|
||||
|
@ -204,6 +207,8 @@ public class MainApp extends Application implements HasAndroidInjector {
|
|||
|
||||
private static AppComponent appComponent;
|
||||
|
||||
private NetworkChangeReceiver networkChangeReceiver;
|
||||
|
||||
/**
|
||||
* Temporary hack
|
||||
*/
|
||||
|
@ -227,6 +232,11 @@ public class MainApp extends Application implements HasAndroidInjector {
|
|||
return powerManagementService;
|
||||
}
|
||||
|
||||
private void registerNetworkChangeReceiver() {
|
||||
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
|
||||
registerReceiver(networkChangeReceiver, filter);
|
||||
}
|
||||
|
||||
private String getAppProcessName() {
|
||||
String processName = "";
|
||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
|
@ -372,9 +382,12 @@ public class MainApp extends Application implements HasAndroidInjector {
|
|||
backgroundJobManager.startMediaFoldersDetectionJob();
|
||||
backgroundJobManager.schedulePeriodicHealthStatus();
|
||||
backgroundJobManager.scheduleInternal2WaySync();
|
||||
backgroundJobManager.startPeriodicallyOfflineOperation();
|
||||
}
|
||||
|
||||
registerGlobalPassCodeProtection();
|
||||
networkChangeReceiver = new NetworkChangeReceiver(this, connectivityService);
|
||||
registerNetworkChangeReceiver();
|
||||
}
|
||||
|
||||
private final LifecycleEventObserver lifecycleEventObserver = ((lifecycleOwner, event) -> {
|
||||
|
@ -670,6 +683,10 @@ public class MainApp extends Application implements HasAndroidInjector {
|
|||
R.string.notification_channel_push_name, R.string
|
||||
.notification_channel_push_description, context, NotificationManager.IMPORTANCE_DEFAULT);
|
||||
|
||||
createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS,
|
||||
R.string.notification_channel_background_operations_name, R.string
|
||||
.notification_channel_background_operations_description, context, NotificationManager.IMPORTANCE_DEFAULT);
|
||||
|
||||
createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_GENERAL, R.string
|
||||
.notification_channel_general_name, R.string.notification_channel_general_description,
|
||||
context, NotificationManager.IMPORTANCE_DEFAULT);
|
||||
|
@ -974,4 +991,16 @@ public class MainApp extends Application implements HasAndroidInjector {
|
|||
case SYSTEM -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void networkAndServerConnectionListener(boolean isNetworkAndServerAvailable) {
|
||||
if (backgroundJobManager == null) {
|
||||
Log_OC.d(TAG, "Offline operations terminated, backgroundJobManager cannot be null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNetworkAndServerAvailable) {
|
||||
backgroundJobManager.startOfflineOperations();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
*/
|
||||
package com.owncloud.android.datamodel;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentProviderResult;
|
||||
|
@ -34,7 +35,14 @@ import com.google.gson.JsonSyntaxException;
|
|||
import com.nextcloud.client.account.User;
|
||||
import com.nextcloud.client.database.NextcloudDatabase;
|
||||
import com.nextcloud.client.database.dao.FileDao;
|
||||
import com.nextcloud.client.database.dao.OfflineOperationDao;
|
||||
import com.nextcloud.client.database.entity.FileEntity;
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity;
|
||||
import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepository;
|
||||
import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepositoryType;
|
||||
import com.nextcloud.model.OfflineOperationType;
|
||||
import com.nextcloud.utils.date.DateFormatPattern;
|
||||
import com.nextcloud.utils.extensions.DateExtensionsKt;
|
||||
import com.owncloud.android.MainApp;
|
||||
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
|
||||
import com.owncloud.android.lib.common.network.WebdavEntry;
|
||||
|
@ -65,6 +73,7 @@ import java.util.ArrayList;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
@ -94,19 +103,23 @@ public class FileDataStorageManager {
|
|||
private final ContentProviderClient contentProviderClient;
|
||||
private final User user;
|
||||
|
||||
public final OfflineOperationDao offlineOperationDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).offlineOperationDao();
|
||||
private final FileDao fileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).fileDao();
|
||||
private final Gson gson = new Gson();
|
||||
private final OfflineOperationsRepositoryType offlineOperationsRepository;
|
||||
|
||||
public FileDataStorageManager(User user, ContentResolver contentResolver) {
|
||||
this.contentProviderClient = null;
|
||||
this.contentResolver = contentResolver;
|
||||
this.user = user;
|
||||
offlineOperationsRepository = new OfflineOperationsRepository(this);
|
||||
}
|
||||
|
||||
public FileDataStorageManager(User user, ContentProviderClient contentProviderClient) {
|
||||
this.contentProviderClient = contentProviderClient;
|
||||
this.contentResolver = null;
|
||||
this.user = user;
|
||||
offlineOperationsRepository = new OfflineOperationsRepository(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -126,6 +139,73 @@ public class FileDataStorageManager {
|
|||
return getFileByPath(ProviderTableMeta.FILE_PATH_DECRYPTED, path);
|
||||
}
|
||||
|
||||
public OfflineOperationEntity addCreateFolderOfflineOperation(String path, String filename, String parentPath, Long parentOCFileId) {
|
||||
OfflineOperationEntity entity = new OfflineOperationEntity();
|
||||
|
||||
entity.setFilename(filename);
|
||||
entity.setParentOCFileId(parentOCFileId);
|
||||
entity.setPath(path);
|
||||
entity.setParentPath(parentPath);
|
||||
entity.setCreatedAt(System.currentTimeMillis() / 1000L);
|
||||
entity.setType(OfflineOperationType.CreateFolder);
|
||||
|
||||
offlineOperationDao.insert(entity);
|
||||
createPendingDirectory(path);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
public void createPendingDirectory(String path) {
|
||||
OCFile file = new OCFile(path);
|
||||
file.setMimeType(MimeType.DIRECTORY);
|
||||
saveFileWithParent(file, MainApp.getAppContext());
|
||||
}
|
||||
|
||||
public void deleteOfflineOperation(OCFile file) {
|
||||
offlineOperationsRepository.deleteOperation(file);
|
||||
}
|
||||
|
||||
public void renameCreateFolderOfflineOperation(OCFile file, String newFolderName) {
|
||||
var entity = offlineOperationDao.getByPath(file.getDecryptedRemotePath());
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
OCFile parentFolder = getFileById(file.getParentId());
|
||||
if (parentFolder == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String newPath = parentFolder.getDecryptedRemotePath() + newFolderName + OCFile.PATH_SEPARATOR;
|
||||
entity.setPath(newPath);
|
||||
entity.setFilename(newFolderName);
|
||||
offlineOperationDao.update(entity);
|
||||
|
||||
moveLocalFile(file, newPath, parentFolder.getDecryptedRemotePath());
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
public void keepOfflineOperationAndServerFile(OfflineOperationEntity entity, OCFile file) {
|
||||
if (file == null) return;
|
||||
|
||||
String oldFileName = entity.getFilename();
|
||||
if (oldFileName == null) return;
|
||||
|
||||
Long parentOCFileId = entity.getParentOCFileId();
|
||||
if (parentOCFileId == null) return;
|
||||
|
||||
OCFile parentFolder = getFileById(parentOCFileId);
|
||||
if (parentFolder == null) return;
|
||||
|
||||
DateFormatPattern formatPattern = DateFormatPattern.FullDateWithHours;
|
||||
String currentDateTime = DateExtensionsKt.currentDateRepresentation(new Date(), formatPattern);
|
||||
|
||||
String newFolderName = oldFileName + " - " + currentDateTime;
|
||||
String newPath = parentFolder.getDecryptedRemotePath() + newFolderName + OCFile.PATH_SEPARATOR;
|
||||
moveLocalFile(file, newPath, parentFolder.getDecryptedRemotePath());
|
||||
offlineOperationsRepository.updateNextOperations(entity);
|
||||
}
|
||||
|
||||
private @Nullable
|
||||
OCFile getFileByPath(String type, String path) {
|
||||
final boolean shouldUseEncryptedPath = ProviderTableMeta.FILE_PATH.equals(type);
|
||||
|
@ -171,7 +251,9 @@ public class FileDataStorageManager {
|
|||
return null;
|
||||
}
|
||||
|
||||
public boolean fileExists(long id) { return fileDao.getFileById(id) != null; }
|
||||
public boolean fileExists(long id) {
|
||||
return fileDao.getFileById(id) != null;
|
||||
}
|
||||
|
||||
public boolean fileExists(String path) {
|
||||
return fileDao.getFileByEncryptedRemotePath(path, user.getAccountName()) != null;
|
||||
|
@ -364,19 +446,19 @@ public class FileDataStorageManager {
|
|||
}
|
||||
|
||||
public static void clearTempEncryptedFolder(String accountName) {
|
||||
File tempEncryptedFolder = new File(FileStorageUtils.getTemporalEncryptedFolderPath(accountName));
|
||||
File tempEncryptedFolder = new File(FileStorageUtils.getTemporalEncryptedFolderPath(accountName));
|
||||
|
||||
if (!tempEncryptedFolder.exists()) {
|
||||
Log_OC.d(TAG,"tempEncryptedFolder does not exist");
|
||||
Log_OC.d(TAG, "tempEncryptedFolder does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
FileUtils.cleanDirectory(tempEncryptedFolder);
|
||||
|
||||
Log_OC.d(TAG,"tempEncryptedFolder cleared");
|
||||
Log_OC.d(TAG, "tempEncryptedFolder cleared");
|
||||
} catch (IOException exception) {
|
||||
Log_OC.d(TAG,"Error caught at clearTempEncryptedFolder: " + exception);
|
||||
Log_OC.d(TAG, "Error caught at clearTempEncryptedFolder: " + exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -409,7 +491,7 @@ public class FileDataStorageManager {
|
|||
|
||||
/**
|
||||
* Inserts or updates the list of files contained in a given folder.
|
||||
*
|
||||
* <p>
|
||||
* CALLER IS RESPONSIBLE FOR GRANTING RIGHT UPDATE OF INFORMATION, NOT THIS METHOD. HERE ONLY DATA CONSISTENCY
|
||||
* SHOULD BE GRANTED
|
||||
*
|
||||
|
@ -457,7 +539,7 @@ public class FileDataStorageManager {
|
|||
whereArgs[1] = ocFile.getRemotePath();
|
||||
if (ocFile.isFolder()) {
|
||||
operations.add(ContentProviderOperation.newDelete(
|
||||
ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, ocFile.getFileId()))
|
||||
ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, ocFile.getFileId()))
|
||||
.withSelection(where, whereArgs).build());
|
||||
|
||||
File localFolder = new File(FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), ocFile));
|
||||
|
@ -466,7 +548,7 @@ public class FileDataStorageManager {
|
|||
}
|
||||
} else {
|
||||
operations.add(ContentProviderOperation.newDelete(
|
||||
ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, ocFile.getFileId()))
|
||||
ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, ocFile.getFileId()))
|
||||
.withSelection(where, whereArgs).build());
|
||||
|
||||
if (ocFile.isDown()) {
|
||||
|
@ -526,6 +608,7 @@ public class FileDataStorageManager {
|
|||
|
||||
/**
|
||||
* Returns a {@link ContentValues} filled with values that are common to both files and folders
|
||||
*
|
||||
* @see #createContentValuesForFile(OCFile)
|
||||
* @see #createContentValuesForFolder(OCFile)
|
||||
*/
|
||||
|
@ -566,6 +649,7 @@ public class FileDataStorageManager {
|
|||
|
||||
/**
|
||||
* Returns a {@link ContentValues} filled with values for a folder
|
||||
*
|
||||
* @see #createContentValuesForFile(OCFile)
|
||||
* @see #createContentValuesBase(OCFile)
|
||||
*/
|
||||
|
@ -577,6 +661,7 @@ public class FileDataStorageManager {
|
|||
|
||||
/**
|
||||
* Returns a {@link ContentValues} filled with values for a file
|
||||
*
|
||||
* @see #createContentValuesForFolder(OCFile)
|
||||
* @see #createContentValuesBase(OCFile)
|
||||
*/
|
||||
|
@ -748,7 +833,7 @@ public class FileDataStorageManager {
|
|||
|
||||
/**
|
||||
* Updates database and file system for a file or folder that was moved to a different location.
|
||||
*
|
||||
* <p>
|
||||
* TODO explore better (faster) implementations TODO throw exceptions up !
|
||||
*/
|
||||
public void moveLocalFile(OCFile ocFile, String targetPath, String targetParentPath) {
|
||||
|
@ -773,7 +858,7 @@ public class FileDataStorageManager {
|
|||
|
||||
int lengthOfOldPath = oldPath.length();
|
||||
int lengthOfOldStoragePath = defaultSavePath.length() + lengthOfOldPath;
|
||||
for (FileEntity fileEntity: fileEntities) {
|
||||
for (FileEntity fileEntity : fileEntities) {
|
||||
ContentValues contentValues = new ContentValues(); // keep construction in the loop
|
||||
OCFile childFile = createFileInstance(fileEntity);
|
||||
contentValues.put(
|
||||
|
@ -876,8 +961,8 @@ public class FileDataStorageManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* This method does not require {@link FileDataStorageManager} being initialized
|
||||
* with any specific user. Migration can be performed with {@link com.nextcloud.client.account.AnonymousUser}.
|
||||
* This method does not require {@link FileDataStorageManager} being initialized with any specific user. Migration
|
||||
* can be performed with {@link com.nextcloud.client.account.AnonymousUser}.
|
||||
*/
|
||||
public void migrateStoredFiles(String sourcePath, String destinationPath)
|
||||
throws RemoteException, OperationApplicationException {
|
||||
|
@ -909,7 +994,7 @@ public class FileDataStorageManager {
|
|||
ContentValues cv = new ContentValues();
|
||||
fileId[0] = String.valueOf(cursor.getLong(cursor.getColumnIndexOrThrow(ProviderTableMeta._ID)));
|
||||
String oldFileStoragePath =
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_STORAGE_PATH));
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_STORAGE_PATH));
|
||||
|
||||
if (oldFileStoragePath.startsWith(sourcePath)) {
|
||||
|
||||
|
@ -940,7 +1025,7 @@ public class FileDataStorageManager {
|
|||
List<OCFile> folderContent = new ArrayList<>();
|
||||
|
||||
List<FileEntity> files = fileDao.getFolderContent(parentId);
|
||||
for (FileEntity fileEntity: files) {
|
||||
for (FileEntity fileEntity : files) {
|
||||
OCFile child = createFileInstance(fileEntity);
|
||||
if (!onlyOnDevice || child.existsOnDevice()) {
|
||||
folderContent.add(child);
|
||||
|
@ -1203,7 +1288,7 @@ public class FileDataStorageManager {
|
|||
+ ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + "=?",
|
||||
new String[]{value, user.getAccountName()},
|
||||
null
|
||||
);
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
cursor = getContentProviderClient().query(
|
||||
|
@ -1212,7 +1297,7 @@ public class FileDataStorageManager {
|
|||
key + AND + ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + "=?",
|
||||
new String[]{value, user.getAccountName()},
|
||||
null
|
||||
);
|
||||
);
|
||||
} catch (RemoteException e) {
|
||||
Log_OC.w(TAG, "Could not get details, assuming share does not exist: " + e.getMessage());
|
||||
cursor = null;
|
||||
|
@ -1451,7 +1536,7 @@ public class FileDataStorageManager {
|
|||
ContentProviderOperation.newInsert(ProviderTableMeta.CONTENT_URI_SHARE)
|
||||
.withValues(contentValues)
|
||||
.build()
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1611,7 +1696,7 @@ public class FileDataStorageManager {
|
|||
ContentProviderOperation.newDelete(ProviderTableMeta.CONTENT_URI_SHARE).
|
||||
withSelection(where, whereArgs).
|
||||
build()
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
return preparedOperations;
|
||||
|
@ -1629,7 +1714,7 @@ public class FileDataStorageManager {
|
|||
.newDelete(ProviderTableMeta.CONTENT_URI_SHARE)
|
||||
.withSelection(where, whereArgs)
|
||||
.build()
|
||||
);
|
||||
);
|
||||
|
||||
return preparedOperations;
|
||||
|
||||
|
@ -1780,7 +1865,7 @@ public class FileDataStorageManager {
|
|||
cv,
|
||||
ProviderTableMeta._ID + "=?",
|
||||
new String[]{String.valueOf(ocFile.getFileId())}
|
||||
);
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
updated = getContentProviderClient().update(
|
||||
|
@ -1788,7 +1873,7 @@ public class FileDataStorageManager {
|
|||
cv,
|
||||
ProviderTableMeta._ID + "=?",
|
||||
new String[]{String.valueOf(ocFile.getFileId())}
|
||||
);
|
||||
);
|
||||
} catch (RemoteException e) {
|
||||
Log_OC.e(TAG, "Failed saving conflict in database " + e.getMessage(), e);
|
||||
}
|
||||
|
@ -1822,7 +1907,7 @@ public class FileDataStorageManager {
|
|||
cv,
|
||||
stringBuilder.toString(),
|
||||
ancestorIds.toArray(new String[]{})
|
||||
);
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
updated = getContentProviderClient().update(
|
||||
|
@ -1830,7 +1915,7 @@ public class FileDataStorageManager {
|
|||
cv,
|
||||
stringBuilder.toString(),
|
||||
ancestorIds.toArray(new String[]{})
|
||||
);
|
||||
);
|
||||
} catch (RemoteException e) {
|
||||
Log_OC.e(TAG, "Failed saving conflict in database " + e.getMessage(), e);
|
||||
}
|
||||
|
@ -1862,7 +1947,7 @@ public class FileDataStorageManager {
|
|||
whereForDescencentsInConflict,
|
||||
new String[]{user.getAccountName(), parentPath + '%'},
|
||||
null
|
||||
);
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
descendentsInConflict = getContentProviderClient().query(
|
||||
|
@ -1871,7 +1956,7 @@ public class FileDataStorageManager {
|
|||
whereForDescencentsInConflict,
|
||||
new String[]{user.getAccountName(), parentPath + "%"},
|
||||
null
|
||||
);
|
||||
);
|
||||
} catch (RemoteException e) {
|
||||
Log_OC.e(TAG, "Failed querying for descendents in conflict " + e.getMessage(), e);
|
||||
}
|
||||
|
@ -1886,7 +1971,7 @@ public class FileDataStorageManager {
|
|||
ProviderTableMeta.FILE_ACCOUNT_OWNER + AND +
|
||||
ProviderTableMeta.FILE_PATH + "=?",
|
||||
new String[]{user.getAccountName(), parentPath}
|
||||
);
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
updated = getContentProviderClient().update(
|
||||
|
@ -1895,7 +1980,7 @@ public class FileDataStorageManager {
|
|||
ProviderTableMeta.FILE_ACCOUNT_OWNER + AND +
|
||||
ProviderTableMeta.FILE_PATH + "=?"
|
||||
, new String[]{user.getAccountName(), parentPath}
|
||||
);
|
||||
);
|
||||
} catch (RemoteException e) {
|
||||
Log_OC.e(TAG, "Failed saving conflict in database " + e.getMessage(), e);
|
||||
}
|
||||
|
@ -2285,7 +2370,7 @@ public class FileDataStorageManager {
|
|||
Log_OC.d(TAG, "getGalleryItems - query complete, list size: " + fileEntities.size());
|
||||
|
||||
List<OCFile> files = new ArrayList<>(fileEntities.size());
|
||||
for (FileEntity fileEntity: fileEntities) {
|
||||
for (FileEntity fileEntity : fileEntities) {
|
||||
files.add(createFileInstance(fileEntity));
|
||||
}
|
||||
|
||||
|
@ -2306,7 +2391,7 @@ public class FileDataStorageManager {
|
|||
ProviderTableMeta.VIRTUAL_TYPE + "=?",
|
||||
new String[]{String.valueOf(type)},
|
||||
null
|
||||
);
|
||||
);
|
||||
} catch (RemoteException e) {
|
||||
Log_OC.e(TAG, e.getMessage(), e);
|
||||
return ocFiles;
|
||||
|
@ -2318,7 +2403,7 @@ public class FileDataStorageManager {
|
|||
ProviderTableMeta.VIRTUAL_TYPE + "=?",
|
||||
new String[]{String.valueOf(type)},
|
||||
null
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
if (c != null) {
|
||||
|
@ -2397,7 +2482,7 @@ public class FileDataStorageManager {
|
|||
List<OCFile> files = getAllFilesRecursivelyInsideFolder(folder);
|
||||
List<Pair<String, String>> decryptedFileNamesAndEncryptedRemotePaths = getDecryptedFileNamesAndEncryptedRemotePaths(files);
|
||||
|
||||
String decryptedFileName = decryptedRemotePath.substring( decryptedRemotePath.lastIndexOf('/') + 1);
|
||||
String decryptedFileName = decryptedRemotePath.substring(decryptedRemotePath.lastIndexOf('/') + 1);
|
||||
|
||||
for (Pair<String, String> item : decryptedFileNamesAndEncryptedRemotePaths) {
|
||||
if (item.getFirst().equals(decryptedFileName)) {
|
||||
|
@ -2434,7 +2519,7 @@ public class FileDataStorageManager {
|
|||
List<FileEntity> fileEntities = fileDao.getAllFiles(user.getAccountName());
|
||||
List<OCFile> folderContent = new ArrayList<>(fileEntities.size());
|
||||
|
||||
for (FileEntity fileEntity: fileEntities) {
|
||||
for (FileEntity fileEntity : fileEntities) {
|
||||
folderContent.add(createFileInstance(fileEntity));
|
||||
}
|
||||
|
||||
|
@ -2483,7 +2568,7 @@ public class FileDataStorageManager {
|
|||
|
||||
return files;
|
||||
}
|
||||
|
||||
|
||||
public List<OCFile> getInternalTwoWaySyncFolders(User user) {
|
||||
List<FileEntity> fileEntities = fileDao.getInternalTwoWaySyncFolders(user.getAccountName());
|
||||
List<OCFile> files = new ArrayList<>(fileEntities.size());
|
||||
|
@ -2494,7 +2579,7 @@ public class FileDataStorageManager {
|
|||
|
||||
return files;
|
||||
}
|
||||
|
||||
|
||||
public boolean isPartOfInternalTwoWaySync(OCFile file) {
|
||||
if (file.isInternalFolderSync()) {
|
||||
return true;
|
||||
|
|
|
@ -35,6 +35,7 @@ import com.owncloud.android.utils.MimeType;
|
|||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -775,6 +776,26 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
|
|||
return this.downloading;
|
||||
}
|
||||
|
||||
public boolean isRootDirectory() {
|
||||
return decryptedRemotePath.equals(ROOT_PATH);
|
||||
}
|
||||
|
||||
public boolean isOfflineOperation() {
|
||||
return getRemoteId() == null;
|
||||
}
|
||||
|
||||
public String getOfflineOperationParentPath() {
|
||||
if (isOfflineOperation()) {
|
||||
if (Objects.equals(remotePath, OCFile.PATH_SEPARATOR)) {
|
||||
return OCFile.PATH_SEPARATOR;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return getDecryptedRemotePath();
|
||||
}
|
||||
}
|
||||
|
||||
public String getEtagInConflict() {
|
||||
return this.etagInConflict;
|
||||
}
|
||||
|
|
|
@ -25,13 +25,14 @@ import java.util.List;
|
|||
*/
|
||||
public class ProviderMeta {
|
||||
public static final String DB_NAME = "filelist";
|
||||
public static final int DB_VERSION = 83;
|
||||
public static final int DB_VERSION = 84;
|
||||
|
||||
private ProviderMeta() {
|
||||
// No instance
|
||||
}
|
||||
|
||||
static public class ProviderTableMeta implements BaseColumns {
|
||||
public static final String OFFLINE_OPERATION_TABLE_NAME = "offline_operations";
|
||||
public static final String FILE_TABLE_NAME = "filelist";
|
||||
public static final String OCSHARES_TABLE_NAME = "ocshares";
|
||||
public static final String CAPABILITIES_TABLE_NAME = "capabilities";
|
||||
|
@ -47,24 +48,24 @@ public class ProviderMeta {
|
|||
private static final String CONTENT_PREFIX = "content://";
|
||||
|
||||
public static final Uri CONTENT_URI = Uri.parse(CONTENT_PREFIX
|
||||
+ MainApp.getAuthority() + "/");
|
||||
+ MainApp.getAuthority() + "/");
|
||||
public static final Uri CONTENT_URI_FILE = Uri.parse(CONTENT_PREFIX
|
||||
+ MainApp.getAuthority() + "/file");
|
||||
+ MainApp.getAuthority() + "/file");
|
||||
public static final Uri CONTENT_URI_DIR = Uri.parse(CONTENT_PREFIX
|
||||
+ MainApp.getAuthority() + "/dir");
|
||||
+ MainApp.getAuthority() + "/dir");
|
||||
public static final Uri CONTENT_URI_SHARE = Uri.parse(CONTENT_PREFIX
|
||||
+ MainApp.getAuthority() + "/shares");
|
||||
+ MainApp.getAuthority() + "/shares");
|
||||
public static final Uri CONTENT_URI_CAPABILITIES = Uri.parse(CONTENT_PREFIX
|
||||
+ MainApp.getAuthority() + "/capabilities");
|
||||
+ MainApp.getAuthority() + "/capabilities");
|
||||
public static final Uri CONTENT_URI_UPLOADS = Uri.parse(CONTENT_PREFIX
|
||||
+ MainApp.getAuthority() + "/uploads");
|
||||
+ MainApp.getAuthority() + "/uploads");
|
||||
public static final Uri CONTENT_URI_SYNCED_FOLDERS = Uri.parse(CONTENT_PREFIX
|
||||
+ MainApp.getAuthority() + "/synced_folders");
|
||||
+ MainApp.getAuthority() + "/synced_folders");
|
||||
public static final Uri CONTENT_URI_EXTERNAL_LINKS = Uri.parse(CONTENT_PREFIX
|
||||
+ MainApp.getAuthority() + "/external_links");
|
||||
+ MainApp.getAuthority() + "/external_links");
|
||||
public static final Uri CONTENT_URI_VIRTUAL = Uri.parse(CONTENT_PREFIX + MainApp.getAuthority() + "/virtual");
|
||||
public static final Uri CONTENT_URI_FILESYSTEM = Uri.parse(CONTENT_PREFIX
|
||||
+ MainApp.getAuthority() + "/filesystem");
|
||||
+ MainApp.getAuthority() + "/filesystem");
|
||||
|
||||
|
||||
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.owncloud.file";
|
||||
|
@ -124,58 +125,58 @@ public class ProviderMeta {
|
|||
public static final String FILE_INTERNAL_TWO_WAY_SYNC_RESULT = "internal_two_way_sync_result";
|
||||
|
||||
public static final List<String> FILE_ALL_COLUMNS = Collections.unmodifiableList(Arrays.asList(
|
||||
_ID,
|
||||
FILE_PARENT,
|
||||
FILE_NAME,
|
||||
FILE_ENCRYPTED_NAME,
|
||||
FILE_CREATION,
|
||||
FILE_MODIFIED,
|
||||
FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA,
|
||||
FILE_CONTENT_LENGTH,
|
||||
FILE_CONTENT_TYPE,
|
||||
FILE_STORAGE_PATH,
|
||||
FILE_PATH,
|
||||
FILE_PATH_DECRYPTED,
|
||||
FILE_ACCOUNT_OWNER,
|
||||
FILE_LAST_SYNC_DATE,
|
||||
FILE_LAST_SYNC_DATE_FOR_DATA,
|
||||
FILE_KEEP_IN_SYNC,
|
||||
FILE_ETAG,
|
||||
FILE_ETAG_ON_SERVER,
|
||||
FILE_SHARED_VIA_LINK,
|
||||
FILE_SHARED_WITH_SHAREE,
|
||||
FILE_PERMISSIONS,
|
||||
FILE_REMOTE_ID,
|
||||
FILE_LOCAL_ID,
|
||||
FILE_UPDATE_THUMBNAIL,
|
||||
FILE_IS_DOWNLOADING,
|
||||
FILE_ETAG_IN_CONFLICT,
|
||||
FILE_FAVORITE,
|
||||
FILE_HIDDEN,
|
||||
FILE_IS_ENCRYPTED,
|
||||
FILE_MOUNT_TYPE,
|
||||
FILE_HAS_PREVIEW,
|
||||
FILE_UNREAD_COMMENTS_COUNT,
|
||||
FILE_OWNER_ID,
|
||||
FILE_OWNER_DISPLAY_NAME,
|
||||
FILE_NOTE,
|
||||
FILE_SHAREES,
|
||||
FILE_RICH_WORKSPACE,
|
||||
FILE_LOCKED,
|
||||
FILE_LOCK_TYPE,
|
||||
FILE_LOCK_OWNER,
|
||||
FILE_LOCK_OWNER_DISPLAY_NAME,
|
||||
FILE_LOCK_OWNER_EDITOR,
|
||||
FILE_LOCK_TIMESTAMP,
|
||||
FILE_LOCK_TIMEOUT,
|
||||
FILE_LOCK_TOKEN,
|
||||
FILE_METADATA_SIZE,
|
||||
FILE_METADATA_LIVE_PHOTO,
|
||||
FILE_E2E_COUNTER,
|
||||
FILE_TAGS,
|
||||
FILE_METADATA_GPS,
|
||||
FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP,
|
||||
FILE_INTERNAL_TWO_WAY_SYNC_RESULT));
|
||||
_ID,
|
||||
FILE_PARENT,
|
||||
FILE_NAME,
|
||||
FILE_ENCRYPTED_NAME,
|
||||
FILE_CREATION,
|
||||
FILE_MODIFIED,
|
||||
FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA,
|
||||
FILE_CONTENT_LENGTH,
|
||||
FILE_CONTENT_TYPE,
|
||||
FILE_STORAGE_PATH,
|
||||
FILE_PATH,
|
||||
FILE_PATH_DECRYPTED,
|
||||
FILE_ACCOUNT_OWNER,
|
||||
FILE_LAST_SYNC_DATE,
|
||||
FILE_LAST_SYNC_DATE_FOR_DATA,
|
||||
FILE_KEEP_IN_SYNC,
|
||||
FILE_ETAG,
|
||||
FILE_ETAG_ON_SERVER,
|
||||
FILE_SHARED_VIA_LINK,
|
||||
FILE_SHARED_WITH_SHAREE,
|
||||
FILE_PERMISSIONS,
|
||||
FILE_REMOTE_ID,
|
||||
FILE_LOCAL_ID,
|
||||
FILE_UPDATE_THUMBNAIL,
|
||||
FILE_IS_DOWNLOADING,
|
||||
FILE_ETAG_IN_CONFLICT,
|
||||
FILE_FAVORITE,
|
||||
FILE_HIDDEN,
|
||||
FILE_IS_ENCRYPTED,
|
||||
FILE_MOUNT_TYPE,
|
||||
FILE_HAS_PREVIEW,
|
||||
FILE_UNREAD_COMMENTS_COUNT,
|
||||
FILE_OWNER_ID,
|
||||
FILE_OWNER_DISPLAY_NAME,
|
||||
FILE_NOTE,
|
||||
FILE_SHAREES,
|
||||
FILE_RICH_WORKSPACE,
|
||||
FILE_LOCKED,
|
||||
FILE_LOCK_TYPE,
|
||||
FILE_LOCK_OWNER,
|
||||
FILE_LOCK_OWNER_DISPLAY_NAME,
|
||||
FILE_LOCK_OWNER_EDITOR,
|
||||
FILE_LOCK_TIMESTAMP,
|
||||
FILE_LOCK_TIMEOUT,
|
||||
FILE_LOCK_TOKEN,
|
||||
FILE_METADATA_SIZE,
|
||||
FILE_METADATA_LIVE_PHOTO,
|
||||
FILE_E2E_COUNTER,
|
||||
FILE_TAGS,
|
||||
FILE_METADATA_GPS,
|
||||
FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP,
|
||||
FILE_INTERNAL_TWO_WAY_SYNC_RESULT));
|
||||
public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME + " collate nocase asc";
|
||||
|
||||
// Columns of ocshares table
|
||||
|
@ -200,7 +201,7 @@ public class ProviderMeta {
|
|||
public static final String OCSHARES_SHARE_LABEL = "share_label";
|
||||
|
||||
public static final String OCSHARES_DEFAULT_SORT_ORDER = OCSHARES_FILE_SOURCE
|
||||
+ " collate nocase asc";
|
||||
+ " collate nocase asc";
|
||||
|
||||
// Columns of capabilities table
|
||||
public static final String CAPABILITIES_ACCOUNT_NAME = "account";
|
||||
|
@ -217,11 +218,11 @@ public class ProviderMeta {
|
|||
public static final String CAPABILITIES_SHARING_PUBLIC_ASK_FOR_OPTIONAL_PASSWORD =
|
||||
"sharing_public_ask_for_optional_password";
|
||||
public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED =
|
||||
"sharing_public_expire_date_enabled";
|
||||
"sharing_public_expire_date_enabled";
|
||||
public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS =
|
||||
"sharing_public_expire_date_days";
|
||||
"sharing_public_expire_date_days";
|
||||
public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED =
|
||||
"sharing_public_expire_date_enforced";
|
||||
"sharing_public_expire_date_enforced";
|
||||
public static final String CAPABILITIES_SHARING_PUBLIC_SEND_MAIL = "sharing_public_send_mail";
|
||||
public static final String CAPABILITIES_SHARING_PUBLIC_UPLOAD = "sharing_public_upload";
|
||||
public static final String CAPABILITIES_SHARING_USER_SEND_MAIL = "sharing_user_send_mail";
|
||||
|
@ -286,6 +287,15 @@ public class ProviderMeta {
|
|||
public static final String UPLOADS_IS_WIFI_ONLY = "is_wifi_only";
|
||||
public static final String UPLOADS_FOLDER_UNLOCK_TOKEN = "folder_unlock_token";
|
||||
|
||||
// Columns of offline operation table
|
||||
public static final String OFFLINE_OPERATION_PARENT_OC_FILE_ID = "offline_operations_parent_oc_file_id";
|
||||
public static final String OFFLINE_OPERATION_PARENT_PATH = "offline_operations_parent_path";
|
||||
public static final String OFFLINE_OPERATION_TYPE = "offline_operations_type";
|
||||
public static final String OFFLINE_OPERATION_PATH = "offline_operations_path";
|
||||
public static final String OFFLINE_OPERATION_CREATED_AT = "offline_operations_created_at";
|
||||
public static final String OFFLINE_OPERATION_FILE_NAME = "offline_operations_file_name";
|
||||
|
||||
|
||||
// Columns of synced folder table
|
||||
public static final String SYNCED_FOLDER_LOCAL_PATH = "local_path";
|
||||
public static final String SYNCED_FOLDER_REMOTE_PATH = "remote_path";
|
||||
|
|
|
@ -169,6 +169,7 @@ public class FileMenuFilter {
|
|||
filterLock(toHide, fileLockingEnabled);
|
||||
filterUnlock(toHide, fileLockingEnabled);
|
||||
filterPinToHome(toHide);
|
||||
filterRetry(toHide);
|
||||
|
||||
return toHide;
|
||||
}
|
||||
|
@ -260,6 +261,12 @@ public class FileMenuFilter {
|
|||
}
|
||||
}
|
||||
|
||||
private void filterRetry(List<Integer> toHide) {
|
||||
if (!files.iterator().next().isOfflineOperation()) {
|
||||
toHide.add(R.id.action_retry);
|
||||
}
|
||||
}
|
||||
|
||||
private void filterEdit(
|
||||
List<Integer> toHide,
|
||||
OCCapability capability
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.google.gson.Gson;
|
|||
import com.nextcloud.android.lib.resources.directediting.DirectEditingObtainRemoteOperation;
|
||||
import com.nextcloud.client.account.User;
|
||||
import com.nextcloud.common.NextcloudClient;
|
||||
import com.nextcloud.utils.extensions.RemoteOperationResultExtensionsKt;
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProvider;
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager;
|
||||
|
@ -42,6 +43,7 @@ import com.owncloud.android.lib.resources.status.E2EVersion;
|
|||
import com.owncloud.android.lib.resources.users.GetPredefinedStatusesRemoteOperation;
|
||||
import com.owncloud.android.lib.resources.users.PredefinedStatus;
|
||||
import com.owncloud.android.syncadapter.FileSyncAdapter;
|
||||
import com.owncloud.android.ui.activity.FileDisplayActivity;
|
||||
import com.owncloud.android.utils.DataHolderUtil;
|
||||
import com.owncloud.android.utils.EncryptionUtils;
|
||||
import com.owncloud.android.utils.FileStorageUtils;
|
||||
|
@ -80,7 +82,7 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
/**
|
||||
* Time stamp for the synchronization process in progress
|
||||
*/
|
||||
private long mCurrentSyncTime;
|
||||
private final long mCurrentSyncTime;
|
||||
|
||||
/**
|
||||
* Remote folder to synchronize
|
||||
|
@ -90,17 +92,17 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
/**
|
||||
* Access to the local database
|
||||
*/
|
||||
private FileDataStorageManager mStorageManager;
|
||||
private final FileDataStorageManager fileDataStorageManager;
|
||||
|
||||
/**
|
||||
* Account where the file to synchronize belongs
|
||||
*/
|
||||
private User user;
|
||||
private final User user;
|
||||
|
||||
/**
|
||||
* Android context; necessary to send requests to the download service
|
||||
*/
|
||||
private Context mContext;
|
||||
private final Context mContext;
|
||||
|
||||
/**
|
||||
* Files and folders contained in the synchronized folder after a successful operation
|
||||
|
@ -121,12 +123,12 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
* Map of remote and local paths to files that where locally stored in a location out of the ownCloud folder and
|
||||
* couldn't be copied automatically into it
|
||||
**/
|
||||
private Map<String, String> mForgottenLocalFiles;
|
||||
private final Map<String, String> mForgottenLocalFiles;
|
||||
|
||||
/**
|
||||
* 'True' means that this operation is part of a full account synchronization
|
||||
*/
|
||||
private boolean mSyncFullAccount;
|
||||
private final boolean mSyncFullAccount;
|
||||
|
||||
/**
|
||||
* 'True' means that the remote folder changed and should be fetched
|
||||
|
@ -136,14 +138,14 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
/**
|
||||
* 'True' means that Etag will be ignored
|
||||
*/
|
||||
private boolean mIgnoreETag;
|
||||
private final boolean mIgnoreETag;
|
||||
|
||||
/**
|
||||
* 'True' means that no share and no capabilities will be updated
|
||||
*/
|
||||
private boolean mOnlyFileMetadata;
|
||||
private final boolean mOnlyFileMetadata;
|
||||
|
||||
private List<SynchronizeFileOperation> mFilesToSyncContents;
|
||||
private final List<SynchronizeFileOperation> mFilesToSyncContents;
|
||||
// this will be used for every file when 'folder synchronization' replaces 'folder download'
|
||||
|
||||
|
||||
|
@ -169,7 +171,7 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
mLocalFolder = folder;
|
||||
mCurrentSyncTime = currentSyncTime;
|
||||
mSyncFullAccount = syncFullAccount;
|
||||
mStorageManager = dataStorageManager;
|
||||
fileDataStorageManager = dataStorageManager;
|
||||
this.user = user;
|
||||
mContext = context;
|
||||
mForgottenLocalFiles = new HashMap<>();
|
||||
|
@ -190,7 +192,7 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
mLocalFolder = folder;
|
||||
mCurrentSyncTime = currentSyncTime;
|
||||
mSyncFullAccount = syncFullAccount;
|
||||
mStorageManager = dataStorageManager;
|
||||
fileDataStorageManager = dataStorageManager;
|
||||
this.user = user;
|
||||
mContext = context;
|
||||
mForgottenLocalFiles = new HashMap<>();
|
||||
|
@ -246,7 +248,7 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
// TODO catch IllegalStateException, show properly to user
|
||||
result = fetchAndSyncRemoteFolder(client);
|
||||
} else {
|
||||
mChildren = mStorageManager.getFolderContent(mLocalFolder, false);
|
||||
mChildren = fileDataStorageManager.getFolderContent(mLocalFolder, false);
|
||||
}
|
||||
|
||||
if (result.isSuccess()) {
|
||||
|
@ -257,13 +259,13 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
}
|
||||
|
||||
mLocalFolder.setLastSyncDateForData(System.currentTimeMillis());
|
||||
mStorageManager.saveFile(mLocalFolder);
|
||||
fileDataStorageManager.saveFile(mLocalFolder);
|
||||
}
|
||||
|
||||
checkFolderConflictData(result);
|
||||
|
||||
if (!mSyncFullAccount && mRemoteFolderChanged) {
|
||||
sendLocalBroadcast(
|
||||
EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result
|
||||
);
|
||||
sendLocalBroadcast(EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result);
|
||||
}
|
||||
|
||||
if (result.isSuccess() && !mSyncFullAccount && !mOnlyFileMetadata) {
|
||||
|
@ -271,13 +273,29 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
}
|
||||
|
||||
if (!mSyncFullAccount) {
|
||||
sendLocalBroadcast(
|
||||
EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result
|
||||
);
|
||||
sendLocalBroadcast(EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static HashMap<String, String> lastConflictData = new HashMap<>();
|
||||
|
||||
private void checkFolderConflictData(RemoteOperationResult result) {
|
||||
var offlineOperations = fileDataStorageManager.offlineOperationDao.getAll();
|
||||
if (offlineOperations.isEmpty()) return;
|
||||
|
||||
var conflictData = RemoteOperationResultExtensionsKt.getConflictedRemoteIdsWithOfflineOperations(result, offlineOperations, fileDataStorageManager);
|
||||
if (conflictData != null && !conflictData.equals(lastConflictData)) {
|
||||
lastConflictData = new HashMap<>(conflictData);
|
||||
sendFolderSyncConflictEventBroadcast(conflictData);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendFolderSyncConflictEventBroadcast(HashMap<String, String> conflictData) {
|
||||
Intent intent = new Intent(FileDisplayActivity.FOLDER_SYNC_CONFLICT);
|
||||
intent.putExtra(FileDisplayActivity.FOLDER_SYNC_CONFLICT_ARG_REMOTE_IDS_TO_OPERATION_PATHS, conflictData);
|
||||
LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
private void updateOCVersion(OwnCloudClient client) {
|
||||
|
@ -293,7 +311,7 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
try {
|
||||
NextcloudClient nextcloudClient = OwnCloudClientFactory.createNextcloudClient(user, mContext);
|
||||
|
||||
RemoteOperationResult<UserInfo> result = new GetUserProfileOperation(mStorageManager).execute(nextcloudClient);
|
||||
RemoteOperationResult<UserInfo> result = new GetUserProfileOperation(fileDataStorageManager).execute(nextcloudClient);
|
||||
if (!result.isSuccess()) {
|
||||
Log_OC.w(TAG, "Couldn't update user profile from server");
|
||||
} else {
|
||||
|
@ -309,9 +327,9 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
String oldDirectEditingEtag = arbitraryDataProvider.getValue(user,
|
||||
ArbitraryDataProvider.DIRECT_EDITING_ETAG);
|
||||
|
||||
RemoteOperationResult result = new GetCapabilitiesOperation(mStorageManager).execute(mContext);
|
||||
RemoteOperationResult result = new GetCapabilitiesOperation(fileDataStorageManager).execute(mContext);
|
||||
if (result.isSuccess()) {
|
||||
String newDirectEditingEtag = mStorageManager.getCapability(user.getAccountName()).getDirectEditingEtag();
|
||||
String newDirectEditingEtag = fileDataStorageManager.getCapability(user.getAccountName()).getDirectEditingEtag();
|
||||
|
||||
if (!oldDirectEditingEtag.equalsIgnoreCase(newDirectEditingEtag)) {
|
||||
updateDirectEditing(arbitraryDataProvider, newDirectEditingEtag);
|
||||
|
@ -430,13 +448,13 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
}
|
||||
|
||||
private void removeLocalFolder() {
|
||||
if (mStorageManager.fileExists(mLocalFolder.getFileId())) {
|
||||
if (fileDataStorageManager.fileExists(mLocalFolder.getFileId())) {
|
||||
String currentSavePath = FileStorageUtils.getSavePath(user.getAccountName());
|
||||
mStorageManager.removeFolder(
|
||||
fileDataStorageManager.removeFolder(
|
||||
mLocalFolder,
|
||||
true,
|
||||
mLocalFolder.isDown() && mLocalFolder.getStoragePath().startsWith(currentSavePath)
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -451,7 +469,7 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
*/
|
||||
private void synchronizeData(List<Object> folderAndFiles) {
|
||||
// get 'fresh data' from the database
|
||||
mLocalFolder = mStorageManager.getFileByPath(mLocalFolder.getRemotePath());
|
||||
mLocalFolder = fileDataStorageManager.getFileByPath(mLocalFolder.getRemotePath());
|
||||
|
||||
if (mLocalFolder == null) {
|
||||
Log_OC.d(TAG,"mLocalFolder cannot be null");
|
||||
|
@ -469,7 +487,7 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
mFilesToSyncContents.clear();
|
||||
|
||||
// if local folder is encrypted, download fresh metadata
|
||||
boolean encryptedAncestor = FileStorageUtils.checkEncryptionStatus(mLocalFolder, mStorageManager);
|
||||
boolean encryptedAncestor = FileStorageUtils.checkEncryptionStatus(mLocalFolder, fileDataStorageManager);
|
||||
mLocalFolder.setEncrypted(encryptedAncestor);
|
||||
|
||||
// update permission
|
||||
|
@ -505,11 +523,11 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
if (object instanceof DecryptedFolderMetadataFileV1) {
|
||||
e2EVersion = E2EVersion.V1_2;
|
||||
localFilesMap = prefillLocalFilesMap((DecryptedFolderMetadataFileV1) object,
|
||||
mStorageManager.getFolderContent(mLocalFolder, false));
|
||||
fileDataStorageManager.getFolderContent(mLocalFolder, false));
|
||||
} else {
|
||||
e2EVersion = E2EVersion.V2_0;
|
||||
localFilesMap = prefillLocalFilesMap((DecryptedFolderMetadataFile) object,
|
||||
mStorageManager.getFolderContent(mLocalFolder, false));
|
||||
fileDataStorageManager.getFolderContent(mLocalFolder, false));
|
||||
|
||||
// update counter
|
||||
if (object != null) {
|
||||
|
@ -537,7 +555,7 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
|
||||
// TODO better implementation is needed
|
||||
if (localFile == null) {
|
||||
localFile = mStorageManager.getFileByPath(updatedFile.getRemotePath());
|
||||
localFile = fileDataStorageManager.getFileByPath(updatedFile.getRemotePath());
|
||||
}
|
||||
|
||||
// add to updatedFile data about LOCAL STATE (not existing in server)
|
||||
|
@ -556,11 +574,11 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
|
||||
// update file name for encrypted files
|
||||
if (e2EVersion == E2EVersion.V1_2) {
|
||||
updateFileNameForEncryptedFileV1(mStorageManager,
|
||||
updateFileNameForEncryptedFileV1(fileDataStorageManager,
|
||||
(DecryptedFolderMetadataFileV1) object,
|
||||
updatedFile);
|
||||
} else {
|
||||
updateFileNameForEncryptedFile(mStorageManager,
|
||||
updateFileNameForEncryptedFile(fileDataStorageManager,
|
||||
(DecryptedFolderMetadataFile) object,
|
||||
updatedFile);
|
||||
if (localFile != null) {
|
||||
|
@ -579,15 +597,15 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
// save updated contents in local database
|
||||
// update file name for encrypted files
|
||||
if (e2EVersion == E2EVersion.V1_2) {
|
||||
updateFileNameForEncryptedFileV1(mStorageManager,
|
||||
updateFileNameForEncryptedFileV1(fileDataStorageManager,
|
||||
(DecryptedFolderMetadataFileV1) object,
|
||||
mLocalFolder);
|
||||
} else {
|
||||
updateFileNameForEncryptedFile(mStorageManager,
|
||||
updateFileNameForEncryptedFile(fileDataStorageManager,
|
||||
(DecryptedFolderMetadataFile) object,
|
||||
mLocalFolder);
|
||||
}
|
||||
mStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
|
||||
fileDataStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
|
||||
|
||||
mChildren = updatedFiles;
|
||||
}
|
||||
|
@ -817,7 +835,7 @@ public class RefreshFolderOperation extends RemoteOperation {
|
|||
shares.add(share);
|
||||
}
|
||||
}
|
||||
mStorageManager.saveSharesInFolder(shares, mLocalFolder);
|
||||
fileDataStorageManager.saveSharesInFolder(shares, mLocalFolder);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
@ -9,12 +9,18 @@
|
|||
*/
|
||||
package com.owncloud.android.ui.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
import com.nextcloud.client.jobs.download.FileDownloadHelper
|
||||
import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsNotificationManager
|
||||
import com.nextcloud.client.jobs.operation.FileOperationHelper
|
||||
import com.nextcloud.client.jobs.upload.FileUploadHelper
|
||||
import com.nextcloud.client.jobs.upload.FileUploadWorker
|
||||
import com.nextcloud.client.jobs.upload.UploadNotificationManager
|
||||
|
@ -22,7 +28,6 @@ import com.nextcloud.model.HTTPStatusCodes
|
|||
import com.nextcloud.utils.extensions.getParcelableArgument
|
||||
import com.nextcloud.utils.extensions.logFileSize
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.datamodel.UploadsStorageManager
|
||||
import com.owncloud.android.db.OCUpload
|
||||
|
@ -34,24 +39,26 @@ import com.owncloud.android.ui.dialog.ConflictsResolveDialog
|
|||
import com.owncloud.android.ui.dialog.ConflictsResolveDialog.Decision
|
||||
import com.owncloud.android.ui.dialog.ConflictsResolveDialog.OnConflictDecisionMadeListener
|
||||
import com.owncloud.android.utils.FileStorageUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Wrapper activity which will be launched if keep-in-sync file will be modified by external application.
|
||||
*/
|
||||
class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener {
|
||||
@JvmField
|
||||
@Inject
|
||||
var uploadsStorageManager: UploadsStorageManager? = null
|
||||
lateinit var uploadsStorageManager: UploadsStorageManager
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var fileStorageManager: FileDataStorageManager? = null
|
||||
lateinit var fileOperationHelper: FileOperationHelper
|
||||
|
||||
private var conflictUploadId: Long = 0
|
||||
private var offlineOperationPath: String? = null
|
||||
private var existingFile: OCFile? = null
|
||||
private var newFile: OCFile? = null
|
||||
private var localBehaviour = FileUploadWorker.LOCAL_BEHAVIOUR_FORGET
|
||||
private lateinit var offlineOperationNotificationManager: OfflineOperationsNotificationManager
|
||||
|
||||
@JvmField
|
||||
var listener: OnConflictDecisionMadeListener? = null
|
||||
|
@ -61,7 +68,7 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
|
|||
|
||||
getArguments(savedInstanceState)
|
||||
|
||||
val upload = uploadsStorageManager?.getUploadById(conflictUploadId)
|
||||
val upload = uploadsStorageManager.getUploadById(conflictUploadId)
|
||||
if (upload != null) {
|
||||
localBehaviour = upload.localAction
|
||||
}
|
||||
|
@ -69,6 +76,7 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
|
|||
// new file was modified locally in file system
|
||||
newFile = file
|
||||
setupOnConflictDecisionMadeListener(upload)
|
||||
offlineOperationNotificationManager = OfflineOperationsNotificationManager(this, viewThemeUtils)
|
||||
}
|
||||
|
||||
private fun getArguments(savedInstanceState: Bundle?) {
|
||||
|
@ -76,7 +84,9 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
|
|||
conflictUploadId = savedInstanceState.getLong(EXTRA_CONFLICT_UPLOAD_ID)
|
||||
existingFile = savedInstanceState.getParcelableArgument(EXTRA_EXISTING_FILE, OCFile::class.java)
|
||||
localBehaviour = savedInstanceState.getInt(EXTRA_LOCAL_BEHAVIOUR)
|
||||
offlineOperationPath = savedInstanceState.getString(EXTRA_OFFLINE_OPERATION_PATH)
|
||||
} else {
|
||||
offlineOperationPath = intent.getStringExtra(EXTRA_OFFLINE_OPERATION_PATH)
|
||||
conflictUploadId = intent.getLongExtra(EXTRA_CONFLICT_UPLOAD_ID, -1)
|
||||
existingFile = intent.getParcelableArgument(EXTRA_EXISTING_FILE, OCFile::class.java)
|
||||
localBehaviour = intent.getIntExtra(EXTRA_LOCAL_BEHAVIOUR, localBehaviour)
|
||||
|
@ -85,69 +95,121 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
|
|||
|
||||
private fun setupOnConflictDecisionMadeListener(upload: OCUpload?) {
|
||||
listener = OnConflictDecisionMadeListener { decision: Decision? ->
|
||||
val file = newFile // local file got changed, so either upload it or replace it again by server
|
||||
|
||||
// local file got changed, so either upload it or replace it again by server
|
||||
val file = newFile
|
||||
|
||||
// version
|
||||
val user = user.orElseThrow { RuntimeException() }
|
||||
when (decision) {
|
||||
Decision.CANCEL -> {}
|
||||
Decision.KEEP_LOCAL -> {
|
||||
upload?.let {
|
||||
FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
|
||||
}
|
||||
FileUploadHelper.instance().uploadUpdatedFile(
|
||||
user,
|
||||
arrayOf(file),
|
||||
localBehaviour,
|
||||
NameCollisionPolicy.OVERWRITE
|
||||
)
|
||||
}
|
||||
|
||||
Decision.KEEP_BOTH -> {
|
||||
upload?.let {
|
||||
FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
|
||||
}
|
||||
FileUploadHelper.instance().uploadUpdatedFile(
|
||||
user,
|
||||
arrayOf(file),
|
||||
localBehaviour,
|
||||
NameCollisionPolicy.RENAME
|
||||
)
|
||||
}
|
||||
|
||||
Decision.KEEP_SERVER -> {
|
||||
if (!shouldDeleteLocal()) {
|
||||
// Overwrite local file
|
||||
file?.let {
|
||||
FileDownloadHelper.instance().downloadFile(
|
||||
getUser().orElseThrow { RuntimeException() },
|
||||
file,
|
||||
conflictUploadId = conflictUploadId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
upload?.let {
|
||||
FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
|
||||
|
||||
UploadNotificationManager(
|
||||
applicationContext,
|
||||
viewThemeUtils
|
||||
).dismissOldErrorNotification(it.remotePath, it.localPath)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
val offlineOperation = if (offlineOperationPath != null) {
|
||||
fileDataStorageManager.offlineOperationDao.getByPath(offlineOperationPath!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
when (decision) {
|
||||
Decision.KEEP_LOCAL -> keepLocal(file, upload, user)
|
||||
Decision.KEEP_BOTH -> keepBoth(file, upload, user)
|
||||
Decision.KEEP_SERVER -> keepServer(file, upload)
|
||||
Decision.KEEP_OFFLINE_FOLDER -> keepOfflineFolder(newFile, offlineOperation)
|
||||
Decision.KEEP_SERVER_FOLDER -> keepServerFile(offlineOperation)
|
||||
Decision.KEEP_BOTH_FOLDER -> keepBothFolder(offlineOperation, newFile)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun keepBothFolder(offlineOperation: OfflineOperationEntity?, serverFile: OCFile?) {
|
||||
offlineOperation ?: return
|
||||
fileDataStorageManager.keepOfflineOperationAndServerFile(offlineOperation, serverFile)
|
||||
backgroundJobManager.startOfflineOperations()
|
||||
offlineOperationNotificationManager.dismissNotification(offlineOperation.id)
|
||||
}
|
||||
|
||||
private fun keepServerFile(offlineOperation: OfflineOperationEntity?) {
|
||||
offlineOperation ?: return
|
||||
fileDataStorageManager.offlineOperationDao.delete(offlineOperation)
|
||||
|
||||
val id = offlineOperation.id ?: return
|
||||
offlineOperationNotificationManager.dismissNotification(id)
|
||||
}
|
||||
|
||||
private fun keepOfflineFolder(serverFile: OCFile?, offlineOperation: OfflineOperationEntity?) {
|
||||
serverFile ?: return
|
||||
offlineOperation ?: return
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val isSuccess = fileOperationHelper.removeFile(serverFile, false, false)
|
||||
if (isSuccess) {
|
||||
backgroundJobManager.startOfflineOperations()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
offlineOperationNotificationManager.dismissNotification(offlineOperation.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun keepLocal(file: OCFile?, upload: OCUpload?, user: User) {
|
||||
upload?.let {
|
||||
FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
|
||||
}
|
||||
|
||||
FileUploadHelper.instance().uploadUpdatedFile(
|
||||
user,
|
||||
arrayOf(file),
|
||||
localBehaviour,
|
||||
NameCollisionPolicy.OVERWRITE
|
||||
)
|
||||
}
|
||||
|
||||
private fun keepBoth(file: OCFile?, upload: OCUpload?, user: User) {
|
||||
upload?.let {
|
||||
FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
|
||||
}
|
||||
|
||||
FileUploadHelper.instance().uploadUpdatedFile(
|
||||
user,
|
||||
arrayOf(file),
|
||||
localBehaviour,
|
||||
NameCollisionPolicy.RENAME
|
||||
)
|
||||
}
|
||||
|
||||
private fun keepServer(file: OCFile?, upload: OCUpload?) {
|
||||
if (!shouldDeleteLocal()) {
|
||||
// Overwrite local file
|
||||
file?.let {
|
||||
FileDownloadHelper.instance().downloadFile(
|
||||
user.orElseThrow { RuntimeException() },
|
||||
file,
|
||||
conflictUploadId = conflictUploadId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
upload?.let {
|
||||
FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
|
||||
|
||||
UploadNotificationManager(
|
||||
applicationContext,
|
||||
viewThemeUtils
|
||||
).dismissOldErrorNotification(it.remotePath, it.localPath)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
existingFile.logFileSize(TAG)
|
||||
outState.putLong(EXTRA_CONFLICT_UPLOAD_ID, conflictUploadId)
|
||||
outState.putParcelable(EXTRA_EXISTING_FILE, existingFile)
|
||||
outState.putInt(EXTRA_LOCAL_BEHAVIOUR, localBehaviour)
|
||||
|
||||
outState.run {
|
||||
putLong(EXTRA_CONFLICT_UPLOAD_ID, conflictUploadId)
|
||||
putParcelable(EXTRA_EXISTING_FILE, existingFile)
|
||||
putInt(EXTRA_LOCAL_BEHAVIOUR, localBehaviour)
|
||||
}
|
||||
}
|
||||
|
||||
override fun conflictDecisionMade(decision: Decision?) {
|
||||
|
@ -157,23 +219,46 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
|
|||
@Suppress("ReturnCount")
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
if (account == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
if (newFile == null) {
|
||||
Log_OC.e(TAG, "No file received")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
offlineOperationPath?.let { path ->
|
||||
newFile?.let { ocFile ->
|
||||
val offlineOperation = fileDataStorageManager.offlineOperationDao.getByPath(path)
|
||||
|
||||
if (offlineOperation == null) {
|
||||
showErrorAndFinish()
|
||||
return
|
||||
}
|
||||
|
||||
val (ft, _) = prepareDialog()
|
||||
val dialog = ConflictsResolveDialog.newInstance(
|
||||
this,
|
||||
offlineOperation,
|
||||
ocFile
|
||||
)
|
||||
dialog.show(ft, "conflictDialog")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (existingFile == null) {
|
||||
val remotePath = fileStorageManager?.retrieveRemotePathConsideringEncryption(newFile) ?: return
|
||||
val remotePath = fileDataStorageManager.retrieveRemotePathConsideringEncryption(newFile) ?: return
|
||||
val operation = ReadFileRemoteOperation(remotePath)
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
Thread {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val result = operation.execute(account, this)
|
||||
val result = operation.execute(account, this@ConflictsResolveActivity)
|
||||
if (result.isSuccess) {
|
||||
existingFile = FileStorageUtils.fillOCFile(result.data[0] as RemoteFile)
|
||||
existingFile?.lastSyncDateForProperties = System.currentTimeMillis()
|
||||
|
@ -186,14 +271,15 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
|
|||
Log_OC.e(TAG, "Error when trying to fetch remote file", e)
|
||||
showErrorAndFinish()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
} else {
|
||||
val remotePath = fileStorageManager?.retrieveRemotePathConsideringEncryption(existingFile) ?: return
|
||||
val remotePath = fileDataStorageManager.retrieveRemotePathConsideringEncryption(existingFile) ?: return
|
||||
startDialog(remotePath)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDialog(remotePath: String) {
|
||||
@SuppressLint("CommitTransaction")
|
||||
private fun prepareDialog(): Pair<FragmentTransaction, User> {
|
||||
val userOptional = user
|
||||
if (!userOptional.isPresent) {
|
||||
Log_OC.e(TAG, "User not present")
|
||||
|
@ -206,13 +292,21 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
|
|||
if (prev != null) {
|
||||
fragmentTransaction.remove(prev)
|
||||
}
|
||||
|
||||
return fragmentTransaction to user.get()
|
||||
}
|
||||
|
||||
private fun startDialog(remotePath: String) {
|
||||
val (ft, user) = prepareDialog()
|
||||
|
||||
if (existingFile != null && storageManager.fileExists(remotePath) && newFile != null) {
|
||||
val dialog = ConflictsResolveDialog.newInstance(
|
||||
existingFile,
|
||||
this,
|
||||
newFile!!,
|
||||
userOptional.get()
|
||||
existingFile!!,
|
||||
user
|
||||
)
|
||||
dialog.show(fragmentTransaction, "conflictDialog")
|
||||
dialog.show(ft, "conflictDialog")
|
||||
} else {
|
||||
// Account was changed to a different one - just finish
|
||||
Log_OC.e(TAG, "Account was changed, finishing")
|
||||
|
@ -222,8 +316,8 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
|
|||
|
||||
private fun showErrorAndFinish(code: Int? = null) {
|
||||
val message = parseErrorMessage(code)
|
||||
runOnUiThread {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(this@ConflictsResolveActivity, message, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
@ -254,18 +348,28 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
|
|||
*/
|
||||
const val EXTRA_LOCAL_BEHAVIOUR = "LOCAL_BEHAVIOUR"
|
||||
const val EXTRA_EXISTING_FILE = "EXISTING_FILE"
|
||||
private const val EXTRA_OFFLINE_OPERATION_PATH = "EXTRA_OFFLINE_OPERATION_PATH"
|
||||
|
||||
private val TAG = ConflictsResolveActivity::class.java.simpleName
|
||||
|
||||
@JvmStatic
|
||||
fun createIntent(file: OCFile?, user: User?, conflictUploadId: Long, flag: Int?, context: Context?): Intent {
|
||||
val intent = Intent(context, ConflictsResolveActivity::class.java)
|
||||
if (flag != null) {
|
||||
intent.flags = intent.flags or flag
|
||||
return Intent(context, ConflictsResolveActivity::class.java).apply {
|
||||
if (flag != null) {
|
||||
flags = flags or flag
|
||||
}
|
||||
putExtra(EXTRA_FILE, file)
|
||||
putExtra(EXTRA_USER, user)
|
||||
putExtra(EXTRA_CONFLICT_UPLOAD_ID, conflictUploadId)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun createIntent(file: OCFile, offlineOperationPath: String, context: Context): Intent {
|
||||
return Intent(context, ConflictsResolveActivity::class.java).apply {
|
||||
putExtra(EXTRA_FILE, file)
|
||||
putExtra(EXTRA_OFFLINE_OPERATION_PATH, offlineOperationPath)
|
||||
}
|
||||
intent.putExtra(EXTRA_FILE, file)
|
||||
intent.putExtra(EXTRA_USER, user)
|
||||
intent.putExtra(EXTRA_CONFLICT_UPLOAD_ID, conflictUploadId)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,10 @@ import android.app.Activity;
|
|||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
|
@ -36,6 +38,8 @@ import com.nextcloud.client.jobs.BackgroundJobManager;
|
|||
import com.nextcloud.client.jobs.download.FileDownloadWorker;
|
||||
import com.nextcloud.client.jobs.upload.FileUploadHelper;
|
||||
import com.nextcloud.client.network.ConnectivityService;
|
||||
import com.nextcloud.receiver.NetworkChangeListener;
|
||||
import com.nextcloud.receiver.NetworkChangeReceiver;
|
||||
import com.nextcloud.utils.EditorUtils;
|
||||
import com.nextcloud.utils.extensions.ActivityExtensionsKt;
|
||||
import com.nextcloud.utils.extensions.BundleExtensionsKt;
|
||||
|
@ -112,7 +116,7 @@ import static com.owncloud.android.ui.activity.FileDisplayActivity.TAG_PUBLIC_LI
|
|||
*/
|
||||
public abstract class FileActivity extends DrawerActivity
|
||||
implements OnRemoteOperationListener, ComponentsGetter, SslUntrustedCertDialog.OnSslUntrustedCertListener,
|
||||
LoadingVersionNumberTask.VersionDevInterface, FileDetailSharingFragment.OnEditShareListener {
|
||||
LoadingVersionNumberTask.VersionDevInterface, FileDetailSharingFragment.OnEditShareListener, NetworkChangeListener {
|
||||
|
||||
public static final String EXTRA_FILE = "com.owncloud.android.ui.activity.FILE";
|
||||
public static final String EXTRA_LIVE_PHOTO_FILE = "com.owncloud.android.ui.activity.LIVE.PHOTO.FILE";
|
||||
|
@ -176,6 +180,13 @@ public abstract class FileActivity extends DrawerActivity
|
|||
@Inject
|
||||
ArbitraryDataProvider arbitraryDataProvider;
|
||||
|
||||
private NetworkChangeReceiver networkChangeReceiver;
|
||||
|
||||
private void registerNetworkChangeReceiver() {
|
||||
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
|
||||
registerReceiver(networkChangeReceiver, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showFiles(boolean onDeviceOnly, boolean personalFiles) {
|
||||
// must be specialized in subclasses
|
||||
|
@ -198,10 +209,11 @@ public abstract class FileActivity extends DrawerActivity
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
networkChangeReceiver = new NetworkChangeReceiver(this, connectivityService);
|
||||
usersAndGroupsSearchConfig.reset();
|
||||
mHandler = new Handler();
|
||||
mFileOperationsHelper = new FileOperationsHelper(this, getUserAccountManager(), connectivityService, editorUtils);
|
||||
User user = null;
|
||||
User user;
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
mFile = BundleExtensionsKt.getParcelableArgument(savedInstanceState, FileActivity.EXTRA_FILE, OCFile.class);
|
||||
|
@ -227,11 +239,15 @@ public abstract class FileActivity extends DrawerActivity
|
|||
mOperationsServiceConnection = new OperationsServiceConnection();
|
||||
bindService(new Intent(this, OperationsService.class), mOperationsServiceConnection,
|
||||
Context.BIND_AUTO_CREATE);
|
||||
registerNetworkChangeReceiver();
|
||||
}
|
||||
|
||||
public void checkInternetConnection() {
|
||||
if (connectivityService != null && connectivityService.isConnected()) {
|
||||
@Override
|
||||
public void networkAndServerConnectionListener(boolean isNetworkAndServerAvailable) {
|
||||
if (isNetworkAndServerAvailable) {
|
||||
hideInfoBox();
|
||||
} else {
|
||||
showInfoBox(R.string.offline_mode);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -266,6 +282,8 @@ public abstract class FileActivity extends DrawerActivity
|
|||
mOperationsServiceBinder = null;
|
||||
}
|
||||
|
||||
unregisterReceiver(networkChangeReceiver);
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
|
|
|
@ -93,6 +93,7 @@ import com.owncloud.android.operations.RenameFileOperation;
|
|||
import com.owncloud.android.operations.SynchronizeFileOperation;
|
||||
import com.owncloud.android.operations.UploadFileOperation;
|
||||
import com.owncloud.android.syncadapter.FileSyncAdapter;
|
||||
import com.owncloud.android.ui.activity.fileDisplayActivity.OfflineFolderConflictManager;
|
||||
import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask;
|
||||
import com.owncloud.android.ui.asynctasks.FetchRemoteFileTask;
|
||||
import com.owncloud.android.ui.asynctasks.GetRemoteFileTask;
|
||||
|
@ -171,6 +172,8 @@ public class FileDisplayActivity extends FileActivity
|
|||
public static final String LIST_GROUPFOLDERS = "LIST_GROUPFOLDERS";
|
||||
public static final int SINGLE_USER_SIZE = 1;
|
||||
public static final String OPEN_FILE = "NC_OPEN_FILE";
|
||||
public static final String FOLDER_SYNC_CONFLICT = "FOLDER_SYNC_CONFLICT";
|
||||
public static final String FOLDER_SYNC_CONFLICT_ARG_REMOTE_IDS_TO_OPERATION_PATHS = "FOLDER_SYNC_CONFLICT_ARG_REMOTE_IDS_TO_OPERATION_PATHS";
|
||||
|
||||
private FilesBinding binding;
|
||||
|
||||
|
@ -278,6 +281,9 @@ public class FileDisplayActivity extends FileActivity
|
|||
initSyncBroadcastReceiver();
|
||||
observeWorkerState();
|
||||
registerRefreshFolderEventReceiver();
|
||||
|
||||
OfflineFolderConflictManager offlineFolderConflictManager = new OfflineFolderConflictManager(this);
|
||||
offlineFolderConflictManager.registerRefreshSearchEventReceiver();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
@ -871,7 +877,7 @@ public class FileDisplayActivity extends FileActivity
|
|||
if (hasEnoughSpaceAvailable) {
|
||||
File file = new File(filesToUpload[0]);
|
||||
File renamedFile;
|
||||
if(requestCode == REQUEST_CODE__UPLOAD_FROM_CAMERA) {
|
||||
if (requestCode == REQUEST_CODE__UPLOAD_FROM_CAMERA) {
|
||||
renamedFile = new File(file.getParent() + PATH_SEPARATOR + FileOperationsHelper.getCapturedImageName());
|
||||
} else {
|
||||
renamedFile = new File(file.getParent() + PATH_SEPARATOR + FileOperationsHelper.getCapturedVideoName());
|
||||
|
@ -1315,7 +1321,6 @@ public class FileDisplayActivity extends FileActivity
|
|||
|
||||
Log_OC.d(TAG, "Setting progress visibility to " + mSyncInProgress);
|
||||
|
||||
|
||||
OCFileListFragment ocFileListFragment = getListOfFilesFragment();
|
||||
if (ocFileListFragment != null) {
|
||||
ocFileListFragment.setLoading(mSyncInProgress);
|
||||
|
@ -1597,10 +1602,24 @@ public class FileDisplayActivity extends FileActivity
|
|||
fileDownloadProgressListener = null;
|
||||
} else if (state instanceof WorkerState.UploadFinished) {
|
||||
refreshList();
|
||||
} else if (state instanceof WorkerState.OfflineOperationsCompleted) {
|
||||
refreshCurrentDirectory();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void refreshCurrentDirectory() {
|
||||
OCFile currentDir = (getCurrentDir() != null) ?
|
||||
getStorageManager().getFileByDecryptedRemotePath(getCurrentDir().getRemotePath()) : null;
|
||||
|
||||
OCFileListFragment fileListFragment =
|
||||
(ActivityExtensionsKt.lastFragment(this) instanceof OCFileListFragment fragment) ? fragment : getListOfFilesFragment();
|
||||
|
||||
if (fileListFragment != null) {
|
||||
fileListFragment.listDirectory(currentDir, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDownloadWorkerState() {
|
||||
if (mWaitingToPreview != null && getStorageManager() != null) {
|
||||
mWaitingToPreview = getStorageManager().getFileById(mWaitingToPreview.getFileId());
|
||||
|
@ -2216,7 +2235,7 @@ public class FileDisplayActivity extends FileActivity
|
|||
syncAndUpdateFolder(true);
|
||||
}
|
||||
|
||||
private void syncAndUpdateFolder(boolean ignoreETag) {
|
||||
public void syncAndUpdateFolder(boolean ignoreETag) {
|
||||
syncAndUpdateFolder(ignoreETag, false);
|
||||
}
|
||||
|
||||
|
|
|
@ -215,15 +215,19 @@ public abstract class ToolbarActivity extends BaseActivity implements Injectable
|
|||
* @param text the text to be displayed
|
||||
*/
|
||||
protected final void showInfoBox(@StringRes int text) {
|
||||
mInfoBox.setVisibility(View.VISIBLE);
|
||||
mInfoBoxMessage.setText(text);
|
||||
if (mInfoBox != null && mInfoBoxMessage != null) {
|
||||
mInfoBox.setVisibility(View.VISIBLE);
|
||||
mInfoBoxMessage.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the toolbar's info box.
|
||||
*/
|
||||
public final void hideInfoBox() {
|
||||
mInfoBox.setVisibility(View.GONE);
|
||||
if (mInfoBox != null) {
|
||||
mInfoBox.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPreviewImageVisibility(boolean isVisibility) {
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.owncloud.android.ui.activity.fileDisplayActivity
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsNotificationManager
|
||||
import com.nextcloud.utils.extensions.getSerializableArgument
|
||||
import com.owncloud.android.ui.activity.FileDisplayActivity
|
||||
|
||||
class OfflineFolderConflictManager(private val activity: FileDisplayActivity) {
|
||||
|
||||
private val notificationManager = OfflineOperationsNotificationManager(activity, activity.viewThemeUtils)
|
||||
|
||||
fun registerRefreshSearchEventReceiver() {
|
||||
val filter = IntentFilter(FileDisplayActivity.FOLDER_SYNC_CONFLICT)
|
||||
LocalBroadcastManager.getInstance(activity).registerReceiver(folderSyncConflictEventReceiver, filter)
|
||||
}
|
||||
|
||||
private val folderSyncConflictEventReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
intent.run {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val map = getSerializableArgument(
|
||||
FileDisplayActivity.FOLDER_SYNC_CONFLICT_ARG_REMOTE_IDS_TO_OPERATION_PATHS,
|
||||
HashMap::class.java
|
||||
) as? HashMap<String, String>
|
||||
|
||||
if (!map.isNullOrEmpty()) {
|
||||
showFolderSyncConflictNotifications(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFolderSyncConflictNotifications(remoteIdsToOperationPaths: HashMap<String, String>) {
|
||||
remoteIdsToOperationPaths.forEach { (remoteId, path) ->
|
||||
val file = activity.storageManager.getFileByRemoteId(remoteId)
|
||||
file?.let {
|
||||
val entity = activity.storageManager.offlineOperationDao.getByPath(path)
|
||||
if (activity.user.isPresent) {
|
||||
notificationManager.showConflictResolveNotification(file, entity, activity.user.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,6 @@ package com.owncloud.android.ui.adapter
|
|||
|
||||
import android.widget.TextView
|
||||
|
||||
internal interface ListGridItemViewHolder : ListGridImageViewHolder {
|
||||
internal interface ListGridItemViewHolder : ListViewHolder {
|
||||
val fileName: TextView
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import android.widget.LinearLayout
|
|||
import android.widget.TextView
|
||||
import com.elyeproj.loaderviewlibrary.LoaderImageView
|
||||
|
||||
interface ListGridImageViewHolder {
|
||||
interface ListViewHolder {
|
||||
val thumbnail: ImageView
|
||||
fun showVideoOverlay()
|
||||
val shimmerThumbnail: LoaderImageView
|
|
@ -16,6 +16,8 @@ import android.annotation.SuppressLint;
|
|||
import android.app.Activity;
|
||||
import android.content.ContentValues;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
@ -23,7 +25,6 @@ import android.text.TextUtils;
|
|||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
|
@ -32,6 +33,7 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole;
|
|||
import com.nextcloud.client.account.User;
|
||||
import com.nextcloud.client.jobs.upload.FileUploadHelper;
|
||||
import com.nextcloud.client.preferences.AppPreferences;
|
||||
import com.nextcloud.utils.extensions.ViewExtensionsKt;
|
||||
import com.owncloud.android.MainApp;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.databinding.GridImageBinding;
|
||||
|
@ -96,25 +98,26 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
|
||||
private static final int showFilenameColumnThreshold = 4;
|
||||
private final String userId;
|
||||
private Activity activity;
|
||||
private AppPreferences preferences;
|
||||
private final Activity activity;
|
||||
private final AppPreferences preferences;
|
||||
private List<OCFile> mFiles = new ArrayList<>();
|
||||
private List<OCFile> mFilesAll = new ArrayList<>();
|
||||
private boolean hideItemOptions;
|
||||
private final List<OCFile> mFilesAll = new ArrayList<>();
|
||||
private final boolean hideItemOptions;
|
||||
private long lastTimestamp;
|
||||
private boolean gridView;
|
||||
public ArrayList<String> listOfHiddenFiles = new ArrayList<>();
|
||||
private FileDataStorageManager mStorageManager;
|
||||
private User user;
|
||||
private OCFileListFragmentInterface ocFileListFragmentInterface;
|
||||
private final OCFileListFragmentInterface ocFileListFragmentInterface;
|
||||
|
||||
|
||||
private OCFile currentDirectory;
|
||||
private static final String TAG = OCFileListAdapter.class.getSimpleName();
|
||||
|
||||
private static final int VIEWTYPE_FOOTER = 0;
|
||||
private static final int VIEWTYPE_ITEM = 1;
|
||||
private static final int VIEWTYPE_IMAGE = 2;
|
||||
private static final int VIEWTYPE_HEADER = 3;
|
||||
private static final int VIEW_TYPE_FOOTER = 0;
|
||||
private static final int VIEW_TYPE_ITEM = 1;
|
||||
private static final int VIEW_TYPE_IMAGE = 2;
|
||||
private static final int VIEW_TYPE_HEADER = 3;
|
||||
|
||||
private boolean onlyOnDevice;
|
||||
private final OCFileListDelegate ocFileListDelegate;
|
||||
|
@ -339,23 +342,23 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (shouldShowHeader() && position == 0) {
|
||||
return VIEWTYPE_HEADER;
|
||||
return VIEW_TYPE_HEADER;
|
||||
}
|
||||
|
||||
if (shouldShowHeader() && position == mFiles.size() + 1 ||
|
||||
(!shouldShowHeader() && position == mFiles.size())) {
|
||||
return VIEWTYPE_FOOTER;
|
||||
return VIEW_TYPE_FOOTER;
|
||||
}
|
||||
|
||||
OCFile item = getItem(position);
|
||||
if (item == null) {
|
||||
return VIEWTYPE_ITEM;
|
||||
return VIEW_TYPE_ITEM;
|
||||
}
|
||||
|
||||
if (MimeTypeUtil.isImageOrVideo(item)) {
|
||||
return VIEWTYPE_IMAGE;
|
||||
return VIEW_TYPE_IMAGE;
|
||||
} else {
|
||||
return VIEWTYPE_ITEM;
|
||||
return VIEW_TYPE_ITEM;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -378,9 +381,9 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
);
|
||||
}
|
||||
}
|
||||
case VIEWTYPE_IMAGE -> {
|
||||
case VIEW_TYPE_IMAGE -> {
|
||||
if (gridView) {
|
||||
return new OCFileListGridImageViewHolder(
|
||||
return new OCFileListViewHolder(
|
||||
GridImageBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)
|
||||
);
|
||||
} else {
|
||||
|
@ -389,12 +392,12 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
);
|
||||
}
|
||||
}
|
||||
case VIEWTYPE_FOOTER -> {
|
||||
case VIEW_TYPE_FOOTER -> {
|
||||
return new OCFileListFooterViewHolder(
|
||||
ListFooterBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)
|
||||
);
|
||||
}
|
||||
case VIEWTYPE_HEADER -> {
|
||||
case VIEW_TYPE_HEADER -> {
|
||||
ListHeaderBinding binding = ListHeaderBinding.inflate(
|
||||
LayoutInflater.from(parent.getContext()),
|
||||
parent,
|
||||
|
@ -421,7 +424,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
PreviewTextFragment.setText(headerViewHolder.getHeaderText(), text, null, activity, true, true, viewThemeUtils);
|
||||
headerViewHolder.getHeaderView().setOnClickListener(v -> ocFileListFragmentInterface.onHeaderClicked());
|
||||
} else {
|
||||
ListGridImageViewHolder gridViewHolder = (ListGridImageViewHolder) holder;
|
||||
ListViewHolder gridViewHolder = (ListViewHolder) holder;
|
||||
OCFile file = getItem(position);
|
||||
|
||||
if (file == null) {
|
||||
|
@ -430,24 +433,24 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
}
|
||||
|
||||
ocFileListDelegate.bindGridViewHolder(gridViewHolder, file, currentDirectory, searchType);
|
||||
checkVisibilityOfMoreButtons(gridViewHolder);
|
||||
ViewExtensionsKt.setVisibleIf(gridViewHolder.getMore(), !isMultiSelect());
|
||||
checkVisibilityOfFileFeaturesLayout(gridViewHolder);
|
||||
|
||||
if (holder instanceof ListItemViewHolder) {
|
||||
bindListItemViewHolder((ListItemViewHolder) gridViewHolder, file);
|
||||
if (holder instanceof ListItemViewHolder itemViewHolder) {
|
||||
bindListItemViewHolder(itemViewHolder, file);
|
||||
}
|
||||
|
||||
if (holder instanceof ListGridItemViewHolder) {
|
||||
bindListGridItemViewHolder((ListGridItemViewHolder) holder, file);
|
||||
checkVisibilityOfMoreButtons((ListGridItemViewHolder) holder);
|
||||
checkVisibilityOfFileFeaturesLayout((ListGridItemViewHolder) holder);
|
||||
if (holder instanceof ListGridItemViewHolder gridItemViewHolder) {
|
||||
bindListGridItemViewHolder(gridItemViewHolder, file);
|
||||
ViewExtensionsKt.setVisibleIf(gridItemViewHolder.getMore(), !isMultiSelect());
|
||||
checkVisibilityOfFileFeaturesLayout(gridItemViewHolder);
|
||||
}
|
||||
|
||||
updateLivePhotoIndicators(gridViewHolder, file);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkVisibilityOfFileFeaturesLayout(ListGridImageViewHolder holder) {
|
||||
private void checkVisibilityOfFileFeaturesLayout(ListViewHolder holder) {
|
||||
int fileFeaturesVisibility = View.GONE;
|
||||
LinearLayout fileFeaturesLayout = holder.getFileFeaturesLayout();
|
||||
|
||||
|
@ -465,19 +468,6 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
fileFeaturesLayout.setVisibility(fileFeaturesVisibility);
|
||||
}
|
||||
|
||||
private void checkVisibilityOfMoreButtons(ListGridImageViewHolder holder) {
|
||||
ImageButton moreButton = holder.getMore();
|
||||
if (moreButton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMultiSelect()) {
|
||||
moreButton.setVisibility(View.GONE);
|
||||
} else {
|
||||
moreButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void mergeOCFilesForLivePhoto() {
|
||||
List<OCFile> filesToRemove = new ArrayList<>();
|
||||
|
||||
|
@ -505,13 +495,13 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
filesToRemove.clear();
|
||||
}
|
||||
|
||||
private void updateLivePhotoIndicators(ListGridImageViewHolder holder, OCFile file) {
|
||||
private void updateLivePhotoIndicators(ListViewHolder holder, OCFile file) {
|
||||
boolean isLivePhoto = file.getLinkedFileIdForLivePhoto() != null;
|
||||
|
||||
if (holder instanceof OCFileListItemViewHolder) {
|
||||
holder.getLivePhotoIndicator().setVisibility(isLivePhoto ? (View.VISIBLE) : (View.GONE));
|
||||
holder.getLivePhotoIndicatorSeparator().setVisibility(isLivePhoto ? (View.VISIBLE) : (View.GONE));
|
||||
} else if (holder instanceof OCFileListGridImageViewHolder) {
|
||||
} else if (holder instanceof OCFileListViewHolder) {
|
||||
holder.getGridLivePhotoIndicator().setVisibility(isLivePhoto ? (View.VISIBLE) : (View.GONE));
|
||||
}
|
||||
}
|
||||
|
@ -529,6 +519,9 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
holder.getFileName().setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
ViewExtensionsKt.setVisibleIf(holder.getShared(), !file.isOfflineOperation());
|
||||
setColorFilterForOfflineOperations(holder, file);
|
||||
}
|
||||
|
||||
private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) {
|
||||
|
@ -599,15 +592,27 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
localSize = localFile.length();
|
||||
}
|
||||
|
||||
holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(localSize));
|
||||
holder.getFileSize().setVisibility(View.VISIBLE);
|
||||
holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
|
||||
|
||||
if (file.isOfflineOperation()) {
|
||||
holder.getFileSize().setText(MainApp.string(R.string.oc_file_list_adapter_offline_operation_description_text));
|
||||
holder.getFileSizeSeparator().setVisibility(View.GONE);
|
||||
} else {
|
||||
holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(localSize));
|
||||
holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
final long fileLength = file.getFileLength();
|
||||
if (fileLength >= 0) {
|
||||
holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(fileLength));
|
||||
holder.getFileSize().setVisibility(View.VISIBLE);
|
||||
holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
|
||||
|
||||
if (file.isOfflineOperation()) {
|
||||
holder.getFileSize().setText(MainApp.string(R.string.oc_file_list_adapter_offline_operation_description_text));
|
||||
holder.getFileSizeSeparator().setVisibility(View.GONE);
|
||||
} else {
|
||||
holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(fileLength));
|
||||
holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
holder.getFileSize().setVisibility(View.GONE);
|
||||
holder.getFileSizeSeparator().setVisibility(View.GONE);
|
||||
|
@ -641,12 +646,32 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
} else {
|
||||
holder.getOverflowMenu().setImageResource(R.drawable.ic_dots_vertical);
|
||||
}
|
||||
|
||||
applyVisualsForOfflineOperations(holder, file);
|
||||
}
|
||||
|
||||
private void applyVisualsForOfflineOperations(ListItemViewHolder holder, OCFile file) {
|
||||
ViewExtensionsKt.setVisibleIf(holder.getShared(), !file.isOfflineOperation());
|
||||
setColorFilterForOfflineOperations(holder, file);
|
||||
}
|
||||
|
||||
private void setColorFilterForOfflineOperations(ListViewHolder holder, OCFile file) {
|
||||
if (!file.isFolder()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.isOfflineOperation()) {
|
||||
holder.getThumbnail().setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_IN);
|
||||
} else {
|
||||
Drawable drawable = viewThemeUtils.platform.tintDrawable(MainApp.getAppContext(), holder.getThumbnail().getDrawable(), ColorRole.PRIMARY);
|
||||
holder.getThumbnail().setImageDrawable(drawable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) {
|
||||
if (holder instanceof ListGridImageViewHolder) {
|
||||
LoaderImageView thumbnailShimmer = ((ListGridImageViewHolder) holder).getShimmerThumbnail();
|
||||
if (holder instanceof ListViewHolder) {
|
||||
LoaderImageView thumbnailShimmer = ((ListViewHolder) holder).getShimmerThumbnail();
|
||||
if (thumbnailShimmer.getVisibility() == View.VISIBLE) {
|
||||
thumbnailShimmer.setImageResource(R.drawable.background);
|
||||
thumbnailShimmer.resetLoader();
|
||||
|
@ -1018,7 +1043,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
@Override
|
||||
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
|
||||
super.onViewRecycled(holder);
|
||||
if (holder instanceof ListGridImageViewHolder listGridImageViewHolder) {
|
||||
if (holder instanceof ListViewHolder listGridImageViewHolder) {
|
||||
LoaderImageView thumbnailShimmer = listGridImageViewHolder.getShimmerThumbnail();
|
||||
DisplayUtils.stopShimmer(thumbnailShimmer, listGridImageViewHolder.getThumbnail());
|
||||
}
|
||||
|
|
|
@ -195,7 +195,7 @@ class OCFileListDelegate(
|
|||
}
|
||||
|
||||
fun bindGridViewHolder(
|
||||
gridViewHolder: ListGridImageViewHolder,
|
||||
gridViewHolder: ListViewHolder,
|
||||
file: OCFile,
|
||||
currentDirectory: OCFile?,
|
||||
searchType: SearchType?
|
||||
|
@ -253,7 +253,7 @@ class OCFileListDelegate(
|
|||
}
|
||||
}
|
||||
|
||||
private fun bindUnreadComments(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
|
||||
private fun bindUnreadComments(file: OCFile, gridViewHolder: ListViewHolder) {
|
||||
if (file.unreadCommentsCount > 0) {
|
||||
gridViewHolder.unreadComments.visibility = View.VISIBLE
|
||||
gridViewHolder.unreadComments.setOnClickListener {
|
||||
|
@ -265,7 +265,7 @@ class OCFileListDelegate(
|
|||
}
|
||||
}
|
||||
|
||||
private fun bindGridItemLayout(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
|
||||
private fun bindGridItemLayout(file: OCFile, gridViewHolder: ListViewHolder) {
|
||||
setItemLayoutBackgroundColor(file, gridViewHolder)
|
||||
setCheckBoxImage(file, gridViewHolder)
|
||||
setItemLayoutOnClickListeners(file, gridViewHolder)
|
||||
|
@ -275,7 +275,7 @@ class OCFileListDelegate(
|
|||
}
|
||||
}
|
||||
|
||||
private fun setItemLayoutOnClickListeners(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
|
||||
private fun setItemLayoutOnClickListeners(file: OCFile, gridViewHolder: ListViewHolder) {
|
||||
gridViewHolder.itemLayout.setOnClickListener { ocFileListFragmentInterface.onItemClicked(file) }
|
||||
|
||||
if (!hideItemOptions) {
|
||||
|
@ -290,7 +290,7 @@ class OCFileListDelegate(
|
|||
}
|
||||
}
|
||||
|
||||
private fun setItemLayoutBackgroundColor(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
|
||||
private fun setItemLayoutBackgroundColor(file: OCFile, gridViewHolder: ListViewHolder) {
|
||||
val cornerRadius = context.resources.getDimension(R.dimen.selected_grid_container_radius)
|
||||
|
||||
val isDarkModeActive = (syncFolderProvider?.preferences?.isDarkModeEnabled == true)
|
||||
|
@ -313,7 +313,7 @@ class OCFileListDelegate(
|
|||
}
|
||||
}
|
||||
|
||||
private fun setCheckBoxImage(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
|
||||
private fun setCheckBoxImage(file: OCFile, gridViewHolder: ListViewHolder) {
|
||||
if (isCheckedFile(file)) {
|
||||
gridViewHolder.checkbox.setImageDrawable(
|
||||
viewThemeUtils.platform.tintDrawable(context, R.drawable.ic_checkbox_marked, ColorRole.PRIMARY)
|
||||
|
@ -323,7 +323,7 @@ class OCFileListDelegate(
|
|||
}
|
||||
}
|
||||
|
||||
private fun bindGridMetadataViews(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
|
||||
private fun bindGridMetadataViews(file: OCFile, gridViewHolder: ListViewHolder) {
|
||||
if (showMetadata) {
|
||||
showLocalFileIndicator(file, gridViewHolder)
|
||||
gridViewHolder.favorite.visibility = if (file.isFavorite) View.VISIBLE else View.GONE
|
||||
|
@ -333,7 +333,7 @@ class OCFileListDelegate(
|
|||
}
|
||||
}
|
||||
|
||||
private fun showLocalFileIndicator(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
|
||||
private fun showLocalFileIndicator(file: OCFile, gridViewHolder: ListViewHolder) {
|
||||
val operationsServiceBinder = transferServiceGetter.operationsServiceBinder
|
||||
|
||||
val icon: Int? = when {
|
||||
|
@ -365,7 +365,7 @@ class OCFileListDelegate(
|
|||
}
|
||||
}
|
||||
|
||||
private fun showShareIcon(gridViewHolder: ListGridImageViewHolder, file: OCFile) {
|
||||
private fun showShareIcon(gridViewHolder: ListViewHolder, file: OCFile) {
|
||||
val sharedIconView = gridViewHolder.shared
|
||||
if (gridViewHolder is OCFileListItemViewHolder || file.unreadCommentsCount == 0) {
|
||||
sharedIconView.visibility = View.VISIBLE
|
||||
|
|
|
@ -16,11 +16,11 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.elyeproj.loaderviewlibrary.LoaderImageView
|
||||
import com.owncloud.android.databinding.GridImageBinding
|
||||
|
||||
internal class OCFileListGridImageViewHolder(var binding: GridImageBinding) :
|
||||
internal class OCFileListViewHolder(var binding: GridImageBinding) :
|
||||
RecyclerView.ViewHolder(
|
||||
binding.root
|
||||
),
|
||||
ListGridImageViewHolder {
|
||||
ListViewHolder {
|
||||
|
||||
override val thumbnail: ImageView
|
||||
get() = binding.thumbnail
|
|
@ -1,11 +1,8 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2020-2022 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2015 ownCloud Inc.
|
||||
* SPDX-FileCopyrightText: 2012 Bartosz Przybylski <bart.p.pl@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only)
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.owncloud.android.ui.dialog
|
||||
|
||||
|
@ -21,10 +18,10 @@ import androidx.fragment.app.DialogFragment
|
|||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.utils.extensions.getParcelableArgument
|
||||
import com.nextcloud.utils.extensions.getSerializableArgument
|
||||
import com.nextcloud.utils.extensions.logFileSize
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.databinding.ConflictResolveDialogBinding
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
|
@ -33,7 +30,10 @@ import com.owncloud.android.datamodel.SyncedFolderProvider
|
|||
import com.owncloud.android.datamodel.ThumbnailsCacheManager.ThumbnailGenerationTask
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.ui.adapter.LocalFileListAdapter
|
||||
import com.owncloud.android.ui.dialog.parcel.ConflictDialogData
|
||||
import com.owncloud.android.ui.dialog.parcel.ConflictFileData
|
||||
import com.owncloud.android.utils.DisplayUtils
|
||||
import com.owncloud.android.utils.MimeTypeUtil
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
@ -44,24 +44,32 @@ import javax.inject.Inject
|
|||
class ConflictsResolveDialog : DialogFragment(), Injectable {
|
||||
private lateinit var binding: ConflictResolveDialogBinding
|
||||
|
||||
private var existingFile: OCFile? = null
|
||||
private var newFile: File? = null
|
||||
var listener: OnConflictDecisionMadeListener? = null
|
||||
private var user: User? = null
|
||||
private val asyncTasks: MutableList<ThumbnailGenerationTask> = ArrayList()
|
||||
private var positiveButton: MaterialButton? = null
|
||||
|
||||
private var data: ConflictDialogData? = null
|
||||
private var user: User? = null
|
||||
private var leftDataFile: File? = null
|
||||
private var rightDataFile: OCFile? = null
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var syncedFolderProvider: SyncedFolderProvider
|
||||
|
||||
@Inject
|
||||
lateinit var fileDataStorageManager: FileDataStorageManager
|
||||
|
||||
enum class Decision {
|
||||
CANCEL,
|
||||
KEEP_BOTH,
|
||||
KEEP_LOCAL,
|
||||
KEEP_SERVER
|
||||
KEEP_SERVER,
|
||||
KEEP_OFFLINE_FOLDER,
|
||||
KEEP_SERVER_FOLDER,
|
||||
KEEP_BOTH_FOLDER
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
|
@ -98,14 +106,13 @@ class ConflictsResolveDialog : DialogFragment(), Injectable {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
existingFile = savedInstanceState.getParcelableArgument(KEY_EXISTING_FILE, OCFile::class.java)
|
||||
newFile = savedInstanceState.getSerializableArgument(KEY_NEW_FILE, File::class.java)
|
||||
user = savedInstanceState.getParcelableArgument(KEY_USER, User::class.java)
|
||||
} else if (arguments != null) {
|
||||
existingFile = arguments.getParcelableArgument(KEY_EXISTING_FILE, OCFile::class.java)
|
||||
newFile = arguments.getSerializableArgument(KEY_NEW_FILE, File::class.java)
|
||||
user = arguments.getParcelableArgument(KEY_USER, User::class.java)
|
||||
val bundle = savedInstanceState ?: arguments
|
||||
|
||||
if (bundle != null) {
|
||||
data = bundle.getParcelableArgument(ARG_CONFLICT_DATA, ConflictDialogData::class.java)
|
||||
leftDataFile = bundle.getSerializableArgument(ARG_LEFT_FILE, File::class.java)
|
||||
rightDataFile = bundle.getParcelableArgument(ARG_RIGHT_FILE, OCFile::class.java)
|
||||
user = bundle.getParcelableArgument(ARG_USER, User::class.java)
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to create conflict dialog", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
@ -113,80 +120,116 @@ class ConflictsResolveDialog : DialogFragment(), Injectable {
|
|||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
existingFile.logFileSize(TAG)
|
||||
newFile.logFileSize(TAG)
|
||||
outState.putParcelable(KEY_EXISTING_FILE, existingFile)
|
||||
outState.putSerializable(KEY_NEW_FILE, newFile)
|
||||
outState.putParcelable(KEY_USER, user)
|
||||
outState.run {
|
||||
putParcelable(ARG_CONFLICT_DATA, data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
binding = ConflictResolveDialogBinding.inflate(requireActivity().layoutInflater)
|
||||
|
||||
viewThemeUtils.platform.themeCheckbox(binding.newCheckbox)
|
||||
viewThemeUtils.platform.themeCheckbox(binding.existingCheckbox)
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(requireActivity())
|
||||
builder.setView(binding.root)
|
||||
.setPositiveButton(R.string.common_ok) { _: DialogInterface?, _: Int ->
|
||||
if (binding.newCheckbox.isChecked && binding.existingCheckbox.isChecked) {
|
||||
listener?.conflictDecisionMade(Decision.KEEP_BOTH)
|
||||
} else if (binding.newCheckbox.isChecked) {
|
||||
listener?.conflictDecisionMade(Decision.KEEP_LOCAL)
|
||||
} else if (binding.existingCheckbox.isChecked) {
|
||||
listener?.conflictDecisionMade(Decision.KEEP_SERVER)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.common_cancel) { _: DialogInterface?, _: Int ->
|
||||
listener?.conflictDecisionMade(Decision.CANCEL)
|
||||
}
|
||||
.setTitle(String.format(getString(R.string.conflict_file_headline), existingFile?.fileName))
|
||||
val builder = createDialogBuilder()
|
||||
|
||||
setupUI()
|
||||
setOnClickListeners()
|
||||
|
||||
viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.existingFileContainer.context, builder)
|
||||
viewThemeUtils.run {
|
||||
platform.themeCheckbox(binding.leftCheckbox)
|
||||
platform.themeCheckbox(binding.rightCheckbox)
|
||||
dialog.colorMaterialAlertDialogBackground(requireContext(), builder)
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
private fun setupUI() {
|
||||
val parentFile = existingFile?.remotePath?.let { File(it).parentFile }
|
||||
if (parentFile != null) {
|
||||
binding.`in`.text = String.format(getString(R.string.in_folder), parentFile.absolutePath)
|
||||
} else {
|
||||
binding.`in`.visibility = View.GONE
|
||||
}
|
||||
private fun createDialogBuilder(): MaterialAlertDialogBuilder {
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(R.string.common_ok) { _: DialogInterface?, _: Int ->
|
||||
okButtonClick()
|
||||
}
|
||||
.setNegativeButton(R.string.common_cancel) { _: DialogInterface?, _: Int ->
|
||||
listener?.conflictDecisionMade(Decision.CANCEL)
|
||||
}
|
||||
.setTitle(data?.folderName)
|
||||
}
|
||||
|
||||
private fun okButtonClick() {
|
||||
binding.run {
|
||||
val isFolderNameNotExists = (data?.folderName == null)
|
||||
val decision = when {
|
||||
leftCheckbox.isChecked && rightCheckbox.isChecked ->
|
||||
if (isFolderNameNotExists) Decision.KEEP_BOTH_FOLDER else Decision.KEEP_BOTH
|
||||
|
||||
leftCheckbox.isChecked ->
|
||||
if (isFolderNameNotExists) Decision.KEEP_OFFLINE_FOLDER else Decision.KEEP_LOCAL
|
||||
|
||||
rightCheckbox.isChecked ->
|
||||
if (isFolderNameNotExists) Decision.KEEP_SERVER_FOLDER else Decision.KEEP_SERVER
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
decision?.let { listener?.conflictDecisionMade(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupUI() {
|
||||
binding.run {
|
||||
data?.let {
|
||||
val (leftData, rightData) = it.checkboxData
|
||||
|
||||
folderName.visibility = if (it.folderName == null) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
folderName.text = it.folderName
|
||||
|
||||
title.visibility = if (it.title == null) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
title.text = it.title
|
||||
|
||||
description.text = it.description
|
||||
|
||||
leftCheckbox.text = leftData.title
|
||||
leftTimestamp.text = leftData.timestamp
|
||||
leftFileSize.text = leftData.fileSize
|
||||
|
||||
rightCheckbox.text = rightData.title
|
||||
rightTimestamp.text = rightData.timestamp
|
||||
rightFileSize.text = rightData.fileSize
|
||||
|
||||
if (leftDataFile != null && rightDataFile != null && user != null) {
|
||||
setThumbnailsForFileConflicts()
|
||||
} else {
|
||||
val folderIcon = MimeTypeUtil.getDefaultFolderIcon(requireContext(), viewThemeUtils)
|
||||
leftThumbnail.setImageDrawable(folderIcon)
|
||||
rightThumbnail.setImageDrawable(folderIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setThumbnailsForFileConflicts() {
|
||||
binding.leftThumbnail.tag = leftDataFile.hashCode()
|
||||
binding.rightThumbnail.tag = rightDataFile.hashCode()
|
||||
|
||||
// set info for new file
|
||||
binding.newSize.text = newFile?.length()?.let { DisplayUtils.bytesToHumanReadable(it) }
|
||||
binding.newTimestamp.text = newFile?.lastModified()?.let { DisplayUtils.getRelativeTimestamp(context, it) }
|
||||
binding.newThumbnail.tag = newFile.hashCode()
|
||||
LocalFileListAdapter.setThumbnail(
|
||||
newFile,
|
||||
binding.newThumbnail,
|
||||
leftDataFile,
|
||||
binding.leftThumbnail,
|
||||
context,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
// set info for existing file
|
||||
binding.existingSize.text = existingFile?.fileLength?.let { DisplayUtils.bytesToHumanReadable(it) }
|
||||
binding.existingTimestamp.text = existingFile?.modificationTimestamp?.let {
|
||||
DisplayUtils.getRelativeTimestamp(
|
||||
context,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
binding.existingThumbnail.tag = existingFile?.fileId
|
||||
DisplayUtils.setThumbnail(
|
||||
existingFile,
|
||||
binding.existingThumbnail,
|
||||
rightDataFile,
|
||||
binding.rightThumbnail,
|
||||
user,
|
||||
FileDataStorageManager(
|
||||
user,
|
||||
requireContext().contentResolver
|
||||
),
|
||||
fileDataStorageManager,
|
||||
asyncTasks,
|
||||
false,
|
||||
context,
|
||||
|
@ -198,31 +241,35 @@ class ConflictsResolveDialog : DialogFragment(), Injectable {
|
|||
}
|
||||
|
||||
private fun setOnClickListeners() {
|
||||
val checkBoxClickListener = View.OnClickListener {
|
||||
positiveButton?.isEnabled = binding.newCheckbox.isChecked || binding.existingCheckbox.isChecked
|
||||
}
|
||||
binding.run {
|
||||
val checkBoxClickListener = View.OnClickListener {
|
||||
positiveButton?.isEnabled = (leftCheckbox.isChecked || rightCheckbox.isChecked)
|
||||
}
|
||||
|
||||
binding.newCheckbox.setOnClickListener(checkBoxClickListener)
|
||||
binding.existingCheckbox.setOnClickListener(checkBoxClickListener)
|
||||
leftCheckbox.setOnClickListener(checkBoxClickListener)
|
||||
rightCheckbox.setOnClickListener(checkBoxClickListener)
|
||||
|
||||
binding.newFileContainer.setOnClickListener {
|
||||
binding.newCheckbox.isChecked = !binding.newCheckbox.isChecked
|
||||
positiveButton?.isEnabled = binding.newCheckbox.isChecked || binding.existingCheckbox.isChecked
|
||||
}
|
||||
binding.existingFileContainer.setOnClickListener {
|
||||
binding.existingCheckbox.isChecked = !binding.existingCheckbox.isChecked
|
||||
positiveButton?.isEnabled = binding.newCheckbox.isChecked || binding.existingCheckbox.isChecked
|
||||
leftFileContainer.setOnClickListener {
|
||||
leftCheckbox.toggle()
|
||||
positiveButton?.isEnabled = (leftCheckbox.isChecked || rightCheckbox.isChecked)
|
||||
}
|
||||
|
||||
rightFileContainer.setOnClickListener {
|
||||
rightCheckbox.toggle()
|
||||
positiveButton?.isEnabled = (leftCheckbox.isChecked || rightCheckbox.isChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showDialog(activity: AppCompatActivity) {
|
||||
val prev = activity.supportFragmentManager.findFragmentByTag("dialog")
|
||||
val ft = activity.supportFragmentManager.beginTransaction()
|
||||
if (prev != null) {
|
||||
ft.remove(prev)
|
||||
activity.supportFragmentManager.beginTransaction().run {
|
||||
if (prev != null) {
|
||||
this.remove(prev)
|
||||
}
|
||||
addToBackStack(null)
|
||||
show(this, "dialog")
|
||||
}
|
||||
ft.addToBackStack(null)
|
||||
show(ft, "dialog")
|
||||
}
|
||||
|
||||
override fun onCancel(dialog: DialogInterface) {
|
||||
|
@ -236,35 +283,103 @@ class ConflictsResolveDialog : DialogFragment(), Injectable {
|
|||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
for (task in asyncTasks) {
|
||||
task.cancel(true)
|
||||
asyncTasks.forEach {
|
||||
it.cancel(true)
|
||||
Log_OC.d(this, "cancel: abort get method directly")
|
||||
task.getMethod?.abort()
|
||||
it.getMethod?.abort()
|
||||
}
|
||||
|
||||
asyncTasks.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ConflictsResolveDialog"
|
||||
private const val KEY_NEW_FILE = "file"
|
||||
private const val KEY_EXISTING_FILE = "ocfile"
|
||||
private const val KEY_USER = "user"
|
||||
private const val ARG_CONFLICT_DATA = "CONFLICT_DATA"
|
||||
private const val ARG_LEFT_FILE = "LEFT_FILE"
|
||||
private const val ARG_RIGHT_FILE = "RIGHT_FILE"
|
||||
private const val ARG_USER = "USER"
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(existingFile: OCFile?, newFile: OCFile, user: User?): ConflictsResolveDialog {
|
||||
val file = File(newFile.storagePath)
|
||||
file.logFileSize(TAG)
|
||||
fun newInstance(context: Context, leftFile: OCFile, rightFile: OCFile, user: User?): ConflictsResolveDialog {
|
||||
val file = File(leftFile.storagePath)
|
||||
val conflictData = getFileConflictData(file, rightFile, context)
|
||||
|
||||
val bundle = Bundle().apply {
|
||||
putParcelable(KEY_EXISTING_FILE, existingFile)
|
||||
putSerializable(KEY_NEW_FILE, file)
|
||||
putParcelable(KEY_USER, user)
|
||||
putParcelable(ARG_CONFLICT_DATA, conflictData)
|
||||
putSerializable(ARG_LEFT_FILE, file)
|
||||
putParcelable(ARG_RIGHT_FILE, rightFile)
|
||||
putParcelable(ARG_USER, user)
|
||||
}
|
||||
|
||||
return ConflictsResolveDialog().apply {
|
||||
arguments = bundle
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(
|
||||
context: Context,
|
||||
offlineOperation: OfflineOperationEntity,
|
||||
rightFile: OCFile
|
||||
): ConflictsResolveDialog {
|
||||
val conflictData = getFolderConflictData(offlineOperation, rightFile, context)
|
||||
|
||||
val bundle = Bundle().apply {
|
||||
putParcelable(ARG_CONFLICT_DATA, conflictData)
|
||||
putParcelable(ARG_RIGHT_FILE, rightFile)
|
||||
}
|
||||
|
||||
return ConflictsResolveDialog().apply {
|
||||
arguments = bundle
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@JvmStatic
|
||||
private fun getFolderConflictData(
|
||||
offlineOperation: OfflineOperationEntity,
|
||||
rightFile: OCFile,
|
||||
context: Context
|
||||
): ConflictDialogData {
|
||||
val folderName = null
|
||||
|
||||
val leftTitle = context.getString(R.string.prefs_synced_folders_local_path_title)
|
||||
val leftTimestamp =
|
||||
DisplayUtils.getRelativeTimestamp(context, offlineOperation.createdAt?.times(1000L) ?: 0)
|
||||
val leftFileSize = DisplayUtils.bytesToHumanReadable(0)
|
||||
val leftCheckBoxData = ConflictFileData(leftTitle, leftTimestamp.toString(), leftFileSize)
|
||||
|
||||
val rightTitle = context.getString(R.string.prefs_synced_folders_remote_path_title)
|
||||
val rightTimestamp = DisplayUtils.getRelativeTimestamp(context, rightFile.modificationTimestamp)
|
||||
val rightFileSize = DisplayUtils.bytesToHumanReadable(rightFile.fileLength)
|
||||
val rightCheckBoxData = ConflictFileData(rightTitle, rightTimestamp.toString(), rightFileSize)
|
||||
|
||||
val title = context.getString(R.string.conflict_folder_headline)
|
||||
val description = context.getString(R.string.conflict_message_description_for_folder)
|
||||
return ConflictDialogData(folderName, title, description, Pair(leftCheckBoxData, rightCheckBoxData))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun getFileConflictData(file: File, rightFile: OCFile, context: Context): ConflictDialogData {
|
||||
val parentFile = File(rightFile.remotePath).parentFile
|
||||
val folderName = if (parentFile != null) {
|
||||
String.format(context.getString(R.string.in_folder), parentFile.absolutePath)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val leftTitle = context.getString(R.string.conflict_local_file)
|
||||
val leftTimestamp = DisplayUtils.getRelativeTimestamp(context, file.lastModified())
|
||||
val leftFileSize = DisplayUtils.bytesToHumanReadable(file.length())
|
||||
val leftCheckBoxData = ConflictFileData(leftTitle, leftTimestamp.toString(), leftFileSize)
|
||||
|
||||
val rightTitle = context.getString(R.string.conflict_server_file)
|
||||
val rightTimestamp = DisplayUtils.getRelativeTimestamp(context, rightFile.modificationTimestamp)
|
||||
val rightFileSize = DisplayUtils.bytesToHumanReadable(rightFile.fileLength)
|
||||
val rightCheckBoxData = ConflictFileData(rightTitle, rightTimestamp.toString(), rightFileSize)
|
||||
|
||||
val title = context.getString(R.string.choose_which_file)
|
||||
val description = context.getString(R.string.conflict_message_description)
|
||||
return ConflictDialogData(folderName, title, description, Pair(leftCheckBoxData, rightCheckBoxData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,22 +19,28 @@ import android.view.View
|
|||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.common.collect.Sets
|
||||
import com.nextcloud.client.account.CurrentAccountProvider
|
||||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.client.network.ConnectivityService
|
||||
import com.nextcloud.utils.extensions.getParcelableArgument
|
||||
import com.nextcloud.utils.fileNameValidator.FileNameValidator
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.databinding.EditBoxDialogBinding
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.lib.resources.status.OCCapability
|
||||
import com.owncloud.android.ui.activity.ComponentsGetter
|
||||
import com.owncloud.android.ui.activity.FileDisplayActivity
|
||||
import com.owncloud.android.utils.DisplayUtils
|
||||
import com.owncloud.android.utils.KeyboardUtils
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -55,9 +61,12 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList
|
|||
lateinit var keyboardUtils: KeyboardUtils
|
||||
|
||||
@Inject
|
||||
lateinit var currentAccount: CurrentAccountProvider
|
||||
lateinit var connectivityService: ConnectivityService
|
||||
|
||||
private var mParentFolder: OCFile? = null
|
||||
@Inject
|
||||
lateinit var accountProvider: CurrentAccountProvider
|
||||
|
||||
private var parentFolder: OCFile? = null
|
||||
private var positiveButton: MaterialButton? = null
|
||||
|
||||
private lateinit var binding: EditBoxDialogBinding
|
||||
|
@ -92,7 +101,7 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList
|
|||
|
||||
@Suppress("EmptyFunctionBlock")
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
mParentFolder = arguments?.getParcelableArgument(ARG_PARENT_FOLDER, OCFile::class.java)
|
||||
parentFolder = arguments?.getParcelableArgument(ARG_PARENT_FOLDER, OCFile::class.java)
|
||||
|
||||
val inflater = requireActivity().layoutInflater
|
||||
binding = EditBoxDialogBinding.inflate(inflater, null, false)
|
||||
|
@ -121,7 +130,7 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList
|
|||
return builder.create()
|
||||
}
|
||||
|
||||
private fun getOCCapability(): OCCapability = fileDataStorageManager.getCapability(currentAccount.user.accountName)
|
||||
private fun getOCCapability(): OCCapability = fileDataStorageManager.getCapability(accountProvider.user.accountName)
|
||||
|
||||
private fun checkFileNameAfterEachType(fileNames: MutableSet<String>) {
|
||||
val newFileName = binding.userInput.text?.toString()?.trim() ?: ""
|
||||
|
@ -173,14 +182,30 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList
|
|||
return
|
||||
}
|
||||
|
||||
val path = mParentFolder?.decryptedRemotePath + newFolderName + OCFile.PATH_SEPARATOR
|
||||
if (requireActivity() is ComponentsGetter) {
|
||||
(requireActivity() as ComponentsGetter).fileOperationsHelper.createFolder(path)
|
||||
val path = parentFolder?.decryptedRemotePath + newFolderName + OCFile.PATH_SEPARATOR
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
if (connectivityService.isNetworkAndServerAvailable()) {
|
||||
(requireActivity() as ComponentsGetter).fileOperationsHelper.createFolder(path)
|
||||
} else {
|
||||
Log_OC.d(TAG, "Network not available, creating offline operation")
|
||||
fileDataStorageManager.addCreateFolderOfflineOperation(
|
||||
path,
|
||||
newFolderName,
|
||||
parentFolder?.offlineOperationParentPath,
|
||||
parentFolder?.fileId
|
||||
)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
val fileDisplayActivity = requireActivity() as? FileDisplayActivity
|
||||
fileDisplayActivity?.syncAndUpdateFolder(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CreateFolderDialogFragment"
|
||||
private const val ARG_PARENT_FOLDER = "PARENT_FOLDER"
|
||||
const val CREATE_FOLDER_FRAGMENT = "CREATE_FOLDER_FRAGMENT"
|
||||
|
||||
|
|
|
@ -18,9 +18,12 @@ import androidx.appcompat.app.AlertDialog
|
|||
import com.google.android.material.button.MaterialButton
|
||||
import com.nextcloud.client.di.Injectable
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.ui.activity.ComponentsGetter
|
||||
import com.owncloud.android.ui.activity.FileDisplayActivity
|
||||
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDialogFragmentListener
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Dialog requiring confirmation before removing a collection of given OCFiles.
|
||||
|
@ -30,14 +33,19 @@ class RemoveFilesDialogFragment : ConfirmationDialogFragment(), ConfirmationDial
|
|||
private var mTargetFiles: Collection<OCFile>? = null
|
||||
private var actionMode: ActionMode? = null
|
||||
|
||||
@Inject
|
||||
lateinit var fileDataStorageManager: FileDataStorageManager
|
||||
|
||||
private var positiveButton: MaterialButton? = null
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
val alertDialog = dialog as AlertDialog? ?: return
|
||||
|
||||
val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as? MaterialButton
|
||||
positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as? MaterialButton
|
||||
positiveButton?.let {
|
||||
viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(positiveButton)
|
||||
viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(it)
|
||||
}
|
||||
|
||||
val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as? MaterialButton
|
||||
|
@ -76,8 +84,22 @@ class RemoveFilesDialogFragment : ConfirmationDialogFragment(), ConfirmationDial
|
|||
}
|
||||
|
||||
private fun removeFiles(onlyLocalCopy: Boolean) {
|
||||
val cg = activity as ComponentsGetter?
|
||||
cg?.fileOperationsHelper?.removeFiles(mTargetFiles, onlyLocalCopy, false)
|
||||
val (offlineFiles, files) = mTargetFiles?.partition { it.isOfflineOperation } ?: Pair(emptyList(), emptyList())
|
||||
|
||||
offlineFiles.forEach {
|
||||
fileDataStorageManager.deleteOfflineOperation(it)
|
||||
}
|
||||
|
||||
if (files.isNotEmpty()) {
|
||||
val cg = activity as ComponentsGetter?
|
||||
cg?.fileOperationsHelper?.removeFiles(files, onlyLocalCopy, false)
|
||||
}
|
||||
|
||||
if (offlineFiles.isNotEmpty()) {
|
||||
val activity = requireActivity() as? FileDisplayActivity
|
||||
activity?.refreshCurrentDirectory()
|
||||
}
|
||||
|
||||
finishActionMode()
|
||||
}
|
||||
|
||||
|
@ -151,7 +173,8 @@ class RemoveFilesDialogFragment : ConfirmationDialogFragment(), ConfirmationDial
|
|||
|
||||
putInt(ARG_POSITIVE_BTN_RES, R.string.file_delete)
|
||||
|
||||
if (containsFolder || containsDown) {
|
||||
val isAnyFileOffline = files.any { it.isOfflineOperation }
|
||||
if ((containsFolder || containsDown) && !isAnyFileOffline) {
|
||||
putInt(ARG_NEGATIVE_BTN_RES, R.string.confirmation_remove_local)
|
||||
}
|
||||
|
||||
|
|
|
@ -24,14 +24,15 @@ import com.google.common.collect.Sets
|
|||
import com.nextcloud.client.account.CurrentAccountProvider
|
||||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.utils.extensions.getParcelableArgument
|
||||
import com.nextcloud.utils.fileNameValidator.FileNameValidator.isFileHidden
|
||||
import com.nextcloud.utils.fileNameValidator.FileNameValidator.checkFileName
|
||||
import com.nextcloud.utils.fileNameValidator.FileNameValidator.isFileHidden
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.databinding.EditBoxDialogBinding
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.resources.status.OCCapability
|
||||
import com.owncloud.android.ui.activity.ComponentsGetter
|
||||
import com.owncloud.android.ui.activity.FileDisplayActivity
|
||||
import com.owncloud.android.utils.DisplayUtils
|
||||
import com.owncloud.android.utils.KeyboardUtils
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
|
@ -144,9 +145,14 @@ class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListen
|
|||
return
|
||||
}
|
||||
|
||||
if (requireActivity() is ComponentsGetter) {
|
||||
val componentsGetter = requireActivity() as ComponentsGetter
|
||||
componentsGetter.getFileOperationsHelper().renameFile(mTargetFile, newFileName)
|
||||
if (mTargetFile?.isOfflineOperation == true) {
|
||||
fileDataStorageManager.renameCreateFolderOfflineOperation(mTargetFile, newFileName)
|
||||
if (requireActivity() is FileDisplayActivity) {
|
||||
val activity = requireActivity() as FileDisplayActivity
|
||||
activity.refreshCurrentDirectory()
|
||||
}
|
||||
} else {
|
||||
(requireActivity() as ComponentsGetter).fileOperationsHelper.renameFile(mTargetFile, newFileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.owncloud.android.ui.dialog.parcel
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.nextcloud.utils.extensions.readParcelableCompat
|
||||
|
||||
data class ConflictDialogData(
|
||||
val folderName: String?,
|
||||
val title: String?,
|
||||
val description: String,
|
||||
val checkboxData: Pair<ConflictFileData, ConflictFileData>
|
||||
) : Parcelable {
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString() ?: "",
|
||||
parcel.readString() ?: "",
|
||||
parcel.readString() ?: "",
|
||||
checkboxData = Pair(
|
||||
parcel.readParcelableCompat(ConflictFileData::class.java.classLoader) ?: ConflictFileData("", "", ""),
|
||||
parcel.readParcelableCompat(ConflictFileData::class.java.classLoader) ?: ConflictFileData("", "", "")
|
||||
)
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(folderName)
|
||||
parcel.writeString(title)
|
||||
parcel.writeString(description)
|
||||
parcel.writeParcelable(checkboxData.first, flags)
|
||||
parcel.writeParcelable(checkboxData.second, flags)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int = 0
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<ConflictDialogData> {
|
||||
override fun createFromParcel(parcel: Parcel): ConflictDialogData = ConflictDialogData(parcel)
|
||||
override fun newArray(size: Int): Array<ConflictDialogData?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
data class ConflictFileData(
|
||||
val title: String,
|
||||
val timestamp: String,
|
||||
val fileSize: String
|
||||
) : Parcelable {
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString() ?: "",
|
||||
parcel.readString() ?: "",
|
||||
parcel.readString() ?: ""
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(title)
|
||||
parcel.writeString(timestamp)
|
||||
parcel.writeString(fileSize)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int = 0
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<ConflictFileData> {
|
||||
override fun createFromParcel(parcel: Parcel): ConflictFileData = ConflictFileData(parcel)
|
||||
override fun newArray(size: Int): Array<ConflictFileData?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
|
@ -452,7 +452,10 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
|
|||
backgroundJobManager);
|
||||
} else if (itemId == R.id.action_set_as_wallpaper) {
|
||||
containerActivity.getFileOperationsHelper().setPictureAs(getFile(), getView());
|
||||
} else if (itemId == R.id.action_encrypted) {// TODO implement or remove
|
||||
} else if (itemId == R.id.action_retry) {
|
||||
backgroundJobManager.startOfflineOperations();
|
||||
} else if (itemId == R.id.action_encrypted) {
|
||||
// TODO implement or remove
|
||||
} else if (itemId == R.id.action_unset_encrypted) {// TODO implement or remove
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,6 +157,7 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog implements In
|
|||
}
|
||||
|
||||
setupClickListener();
|
||||
filterActionsForOfflineOperations();
|
||||
}
|
||||
|
||||
private void setupClickListener() {
|
||||
|
@ -210,6 +211,22 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog implements In
|
|||
});
|
||||
}
|
||||
|
||||
private void filterActionsForOfflineOperations() {
|
||||
if (!file.isOfflineOperation() || file.isRootDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
binding.menuCreateRichWorkspace.setVisibility(View.GONE);
|
||||
binding.menuUploadFromApp.setVisibility(View.GONE);
|
||||
binding.menuDirectCameraUpload.setVisibility(View.GONE);
|
||||
binding.menuScanDocUpload.setVisibility(View.GONE);
|
||||
binding.menuUploadFiles.setVisibility(View.GONE);
|
||||
binding.menuNewDocument.setVisibility(View.GONE);
|
||||
binding.menuNewSpreadsheet.setVisibility(View.GONE);
|
||||
binding.menuNewPresentation.setVisibility(View.GONE);
|
||||
binding.creatorsContainer.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
|
|
@ -123,6 +123,7 @@ import org.greenrobot.eventbus.ThreadMode;
|
|||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
@ -628,22 +629,28 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
|||
|
||||
public void openActionsMenu(final int filesCount, final Set<OCFile> checkedFiles, final boolean isOverflow) {
|
||||
throttler.run("overflowClick", () -> {
|
||||
final FragmentManager childFragmentManager = getChildFragmentManager();
|
||||
|
||||
List<Integer> toHide = new ArrayList<>();
|
||||
|
||||
for (OCFile file : checkedFiles) {
|
||||
if (file.isOfflineOperation()) {
|
||||
toHide = new ArrayList<>(
|
||||
Arrays.asList(R.id.action_favorite, R.id.action_move_or_copy, R.id.action_sync_file, R.id.action_encrypted, R.id.action_unset_encrypted)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isAPKorAAB(checkedFiles)) {
|
||||
toHide.add(R.id.action_send_share_file);
|
||||
toHide.add(R.id.action_export_file);
|
||||
toHide.add(R.id.action_sync_file);
|
||||
toHide.add(R.id.action_download_file);
|
||||
}
|
||||
|
||||
|
||||
final FragmentManager childFragmentManager = getChildFragmentManager();
|
||||
FileActionsBottomSheet.newInstance(filesCount, checkedFiles, isOverflow, toHide)
|
||||
.setResultListener(childFragmentManager, this, (id) -> {
|
||||
onFileActionChosen(id, checkedFiles);
|
||||
})
|
||||
.setResultListener(childFragmentManager, this, (id) -> onFileActionChosen(id, checkedFiles))
|
||||
.show(childFragmentManager, "actions");
|
||||
;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1108,10 +1115,6 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
|||
@Override
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
public void onItemClicked(OCFile file) {
|
||||
if (mContainerActivity != null && mContainerActivity instanceof FileActivity fileActivity) {
|
||||
fileActivity.checkInternetConnection();
|
||||
}
|
||||
|
||||
if (getCommonAdapter() != null && getCommonAdapter().isMultiSelect()) {
|
||||
toggleItemToCheckedList(file);
|
||||
} else {
|
||||
|
@ -1240,6 +1243,9 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
|||
} else if (itemId == R.id.action_pin_to_homescreen) {
|
||||
shortcutUtil.addShortcutToHomescreen(singleFile, viewThemeUtils, accountManager.getUser(), syncedFolderProvider);
|
||||
return true;
|
||||
} else if (itemId == R.id.action_retry) {
|
||||
backgroundJobManager.startOfflineOperations();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1478,7 +1484,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
|||
}
|
||||
|
||||
// FAB
|
||||
setFabEnabled(mFile != null && mFile.canWrite());
|
||||
setFabEnabled(mFile != null && (mFile.canWrite() || mFile.isOfflineOperation()));
|
||||
|
||||
invalidateActionMode();
|
||||
}
|
||||
|
@ -1980,6 +1986,9 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
|||
*/
|
||||
public void selectAllFiles(boolean select) {
|
||||
OCFileListAdapter ocFileListAdapter = (OCFileListAdapter) getRecyclerView().getAdapter();
|
||||
if (ocFileListAdapter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (select) {
|
||||
ocFileListAdapter.addAllFilesToCheckedFiles();
|
||||
|
|
|
@ -33,6 +33,7 @@ public final class NotificationUtils {
|
|||
public static final String NOTIFICATION_CHANNEL_FILE_SYNC = "NOTIFICATION_CHANNEL_FILE_SYNC";
|
||||
public static final String NOTIFICATION_CHANNEL_FILE_OBSERVER = "NOTIFICATION_CHANNEL_FILE_OBSERVER";
|
||||
public static final String NOTIFICATION_CHANNEL_PUSH = "NOTIFICATION_CHANNEL_PUSH";
|
||||
public static final String NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS = "NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS";
|
||||
|
||||
private NotificationUtils() {
|
||||
// utility class -> private constructor
|
||||
|
|
24
app/src/main/res/drawable/ic_retry.xml
Normal file
24
app/src/main/res/drawable/ic_retry.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<!--
|
||||
~ Nextcloud - Android Client
|
||||
~
|
||||
~ SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
~ SPDX-FileCopyrightText: 2024 Nextcloud GmbH
|
||||
~ SPDX-FileCopyrightText: 2018-2024 Google LLC
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,5V2L8,6l4,4V7c3.31,0 6,2.69 6,6c0,2.97 -2.17,5.43 -5,5.91v2.02c3.95,-0.49 7,-3.85 7,-7.93C20,8.58 16.42,5 12,5z" />
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,13c0,-1.65 0.67,-3.15 1.76,-4.24L6.34,7.34C4.9,8.79 4,10.79 4,13c0,4.08 3.05,7.44 7,7.93v-2.02C8.17,18.43 6,15.97 6,13z" />
|
||||
|
||||
</vector>
|
|
@ -17,44 +17,49 @@
|
|||
android:paddingBottom="@dimen/standard_padding">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/in"
|
||||
android:id="@+id/folder_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/in_folder"
|
||||
android:paddingBottom="@dimen/standard_padding" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_marginTop="@dimen/standard_margin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/choose_which_file"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_marginTop="@dimen/standard_margin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/conflict_message_description" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_marginTop="@dimen/standard_margin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/newFileContainer"
|
||||
android:id="@+id/leftFileContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatCheckBox
|
||||
android:id="@+id/new_checkbox"
|
||||
android:id="@+id/left_checkbox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/conflict_local_file" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/new_thumbnail"
|
||||
android:id="@+id/left_thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_margin="@dimen/standard_half_margin"
|
||||
|
@ -62,33 +67,33 @@
|
|||
android:contentDescription="@string/thumbnail_for_new_file_desc" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/new_timestamp"
|
||||
android:id="@+id/left_timestamp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="12. Dec 2020 - 23:10:20" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/new_size"
|
||||
android:id="@+id/left_file_size"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="5 Mb" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/existingFileContainer"
|
||||
android:id="@+id/rightFileContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatCheckBox
|
||||
android:id="@+id/existing_checkbox"
|
||||
android:id="@+id/right_checkbox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/conflict_server_file" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/existing_thumbnail"
|
||||
android:id="@+id/right_thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_margin="@dimen/standard_half_margin"
|
||||
|
@ -96,13 +101,13 @@
|
|||
android:contentDescription="@string/thumbnail_for_existing_file_description" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/existing_timestamp"
|
||||
android:id="@+id/right_timestamp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="10. Dec 2020 - 10:10:10" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/existing_size"
|
||||
android:id="@+id/right_file_size"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="3 Mb" />
|
||||
|
|
|
@ -179,7 +179,6 @@
|
|||
<string name="confirmation_remove_folders_alert">هل توَدُّ حقاً حذف العناصر المختارة وما يحتوّه؟</string>
|
||||
<string name="confirmation_remove_local">محلياً فقط</string>
|
||||
<string name="conflict_dialog_error">تعذّر إنشاء نافذة حوار حل التعارضات</string>
|
||||
<string name="conflict_file_headline">ملف متضارب %1$s </string>
|
||||
<string name="conflict_local_file">ملف محلي</string>
|
||||
<string name="conflict_message_description">إذا قمت باختيار كلا الاصدارين, الملف المحلي سيحتوي على رقم ملحق باسم الملف.</string>
|
||||
<string name="conflict_server_file">ملف على الخادم</string>
|
||||
|
|
|
@ -179,7 +179,6 @@
|
|||
<string name="confirmation_remove_folders_alert">Do you really want to delete the selected items and their contents?</string>
|
||||
<string name="confirmation_remove_local">Local only</string>
|
||||
<string name="conflict_dialog_error">Conflict resolver dialog cannot be created</string>
|
||||
<string name="conflict_file_headline">Conflicting file %1$s</string>
|
||||
<string name="conflict_local_file">Local file</string>
|
||||
<string name="conflict_message_description">If you select both versions, the local file will have a number appended to its name.</string>
|
||||
<string name="conflict_server_file">Server file</string>
|
||||
|
|
|
@ -153,7 +153,6 @@
|
|||
<string name="confirmation_remove_folder_alert">Наистина ли желаете %1$s и съдържанието ѝ да бъдат изтрито?</string>
|
||||
<string name="confirmation_remove_folders_alert">Наистина ли желаете избраните елементи и съдържанието им да бъдат премахнати?</string>
|
||||
<string name="confirmation_remove_local">Само локално</string>
|
||||
<string name="conflict_file_headline">Несъвместим файл %1$s</string>
|
||||
<string name="conflict_local_file">Локален файл</string>
|
||||
<string name="conflict_message_description">Ако изберете и двете версии, ще бъде добавен номер към името на локалния файл.</string>
|
||||
<string name="conflict_server_file">Файл на сървъра</string>
|
||||
|
|
|
@ -179,7 +179,6 @@
|
|||
<string name="confirmation_remove_folders_alert">Ha sur oc\'h e fell deoc\'h dilemel an elfennoù diuzet ha kement tra zo e-barzh . </string>
|
||||
<string name="confirmation_remove_local">Lec\'hel hepken</string>
|
||||
<string name="conflict_dialog_error">N\'eus ket bet gallet krouiñ un egorenn diskoulmañ tabutoù</string>
|
||||
<string name="conflict_file_headline">Restr kudennek %1$s</string>
|
||||
<string name="conflict_local_file">Restr lec\'hel</string>
|
||||
<string name="conflict_message_description">Ma tibabit an daou stumm e vo staget un niverenn ouzh anv ar restr lec\'hel</string>
|
||||
<string name="conflict_server_file">Restr ar servijer</string>
|
||||
|
|
|
@ -152,7 +152,6 @@
|
|||
<string name="confirmation_remove_folder_alert">Esteu segur que voleu suprimir %1$s i els seus continguts?</string>
|
||||
<string name="confirmation_remove_folders_alert">Esteu segur que voleu suprimir els elements seleccionats i el seu contingut?</string>
|
||||
<string name="confirmation_remove_local">Només local</string>
|
||||
<string name="conflict_file_headline">Fitxer en conflicte %1$s</string>
|
||||
<string name="conflict_local_file">Fitxer local</string>
|
||||
<string name="conflict_message_description">Si seleccioneu ambdues versions, s\'afegirà un numero al nom del fitxer local.</string>
|
||||
<string name="conflict_server_file">Fitxer del servidor</string>
|
||||
|
|
|
@ -170,7 +170,6 @@
|
|||
<string name="confirmation_remove_folder_alert">Opravdu chcete %1$s a jeho obsah odstranit?</string>
|
||||
<string name="confirmation_remove_folders_alert">Opravdu chcete vybrané položky a jejich obsah odstranit?</string>
|
||||
<string name="confirmation_remove_local">Pouze místní</string>
|
||||
<string name="conflict_file_headline">Kolidující soubor %1$s</string>
|
||||
<string name="conflict_local_file">Místní soubor</string>
|
||||
<string name="conflict_message_description">Pokud zvolíte obě verze, k názvu místního souboru bude připojeno číslo.</string>
|
||||
<string name="conflict_server_file">Soubor na serveru</string>
|
||||
|
|
|
@ -164,7 +164,6 @@
|
|||
<string name="confirmation_remove_folder_alert">Er du sikker på at du vil slette %1$s med indhold?</string>
|
||||
<string name="confirmation_remove_folders_alert">Er du sikker på at du vil slette de valgte artikler med indhold?</string>
|
||||
<string name="confirmation_remove_local">Kun lokal</string>
|
||||
<string name="conflict_file_headline">Fil i konflikt %1$s</string>
|
||||
<string name="conflict_local_file">Lokal fil</string>
|
||||
<string name="conflict_message_description">Hvis du vælger begge versioner, vil den lokale fil få tilføjet et nummer til sit navn.</string>
|
||||
<string name="conflict_server_file">Server fil</string>
|
||||
|
|
|
@ -179,7 +179,6 @@
|
|||
<string name="confirmation_remove_folders_alert">Möchten Sie die ausgewählten Elemente und deren inhalt wirklich löschen?</string>
|
||||
<string name="confirmation_remove_local">Nur lokal</string>
|
||||
<string name="conflict_dialog_error">Konfliktlösungsdialog konnte nicht erstellt werden</string>
|
||||
<string name="conflict_file_headline">Konflikt-Datei %1$s</string>
|
||||
<string name="conflict_local_file">Lokale Datei</string>
|
||||
<string name="conflict_message_description">Falls beide Versionen gewählt werden, wird bei der lokalen Datei eine Zahl am Ende des Dateinamens hinzugefügt.</string>
|
||||
<string name="conflict_server_file">Server-Datei</string>
|
||||
|
|
|
@ -151,7 +151,6 @@
|
|||
<string name="confirmation_remove_folder_alert">Θέλετε σίγουρα να διαγράψετε το %1$s και τα περιεχόμενά του;</string>
|
||||
<string name="confirmation_remove_folders_alert">Θέλετε να διαγράψετε τα επιλεγμένα αντικείμενα και τα περιεχόμενά τους;</string>
|
||||
<string name="confirmation_remove_local">Μόνο τοπικά</string>
|
||||
<string name="conflict_file_headline">Αρχείο σε αντίφαση %1$s</string>
|
||||
<string name="conflict_local_file">Τοπικό αρχείο</string>
|
||||
<string name="conflict_message_description">Εάν επιλέξετε και τις δύο εκδόσεις, στο όνομα του τοπικού αρχείου θα προστεθεί ένας αριθμός.</string>
|
||||
<string name="conflict_server_file">Αρχείο διακομιστή</string>
|
||||
|
|
|
@ -179,7 +179,6 @@
|
|||
<string name="confirmation_remove_folders_alert">¿Realmente desea eliminar los elementos seleccionados y sus contenidos?</string>
|
||||
<string name="confirmation_remove_local">Sólo local</string>
|
||||
<string name="conflict_dialog_error">El diálogo de resolución de conflictos no puede ser creado</string>
|
||||
<string name="conflict_file_headline">Archivo en conflicto %1$s</string>
|
||||
<string name="conflict_local_file">Archivo local</string>
|
||||
<string name="conflict_message_description">Si selecciona ambas versiones, se le agregará un número al nombre del archivo copiado.</string>
|
||||
<string name="conflict_server_file">Archivo del servidor</string>
|
||||
|
|
|
@ -153,7 +153,6 @@
|
|||
<string name="confirmation_remove_folder_alert">¿Realmente quieres eliminar %1$s y sus contenidos? </string>
|
||||
<string name="confirmation_remove_folders_alert">¿Reamente deseas eliminar los elementos seleccionados y sus contenidos?</string>
|
||||
<string name="confirmation_remove_local">Sólo local</string>
|
||||
<string name="conflict_file_headline">Archivo en conflicto %1$s</string>
|
||||
<string name="conflict_local_file">Archivo local</string>
|
||||
<string name="conflict_message_description">Si selecciona ambas versiones, el archivo local tendrá un número agregado a su nombre.</string>
|
||||
<string name="conflict_server_file">Archivo del servidor</string>
|
||||
|
|
|
@ -179,7 +179,6 @@
|
|||
<string name="confirmation_remove_folders_alert">¿Reamente deseas eliminar los elementos seleccionados y sus contenidos?</string>
|
||||
<string name="confirmation_remove_local">Sólo local</string>
|
||||
<string name="conflict_dialog_error">El diálogo de resolución de conflictos no puede ser creado</string>
|
||||
<string name="conflict_file_headline">Archivo conflictivo %1$s</string>
|
||||
<string name="conflict_local_file">Archivo local</string>
|
||||
<string name="conflict_message_description">Si seleccionas ambas versiones, el archivo local tendrá un número al final del nombre.</string>
|
||||
<string name="conflict_server_file">Archivo del servidor</string>
|
||||
|
|
|
@ -178,7 +178,6 @@
|
|||
<string name="confirmation_remove_folders_alert">¿Estás seguro de que quieres eliminar los elementos seleccionados y sus contenidos?</string>
|
||||
<string name="confirmation_remove_local">Solo local</string>
|
||||
<string name="conflict_dialog_error">El diálogo de resolución de conflictos no puede ser creado</string>
|
||||
<string name="conflict_file_headline">Conflicto en archivo %1$s</string>
|
||||
<string name="conflict_local_file">Archivo local</string>
|
||||
<string name="conflict_message_description">Si seleccionas ambas versiones, el archivo local tendrá un número añadido a su nombre.</string>
|
||||
<string name="conflict_server_file">Archivo del servidor</string>
|
||||
|
|
|
@ -179,7 +179,6 @@
|
|||
<string name="confirmation_remove_folders_alert">Ziur zaude hautatutako elementuak eta beren edukiak ezabatu nahi dituzula?</string>
|
||||
<string name="confirmation_remove_local">Lokala bakarrik</string>
|
||||
<string name="conflict_dialog_error">Gatazkak konpontzeko elkarrizketa-koadroa ezin da sortu</string>
|
||||
<string name="conflict_file_headline">%1$sfitxategi gatazkatsua</string>
|
||||
<string name="conflict_local_file">Fitxategi lokala</string>
|
||||
<string name="conflict_message_description">Bi bertsioak hautatzen badituzu, fitxategi lokalaren izenari zenbaki bat gehituko zaio.</string>
|
||||
<string name="conflict_server_file">Zerbitzariko fitxategia</string>
|
||||
|
|
|
@ -158,7 +158,6 @@
|
|||
<string name="confirmation_remove_folder_alert">آیا واقعا می خواهید %1$s و محتویات آن را حذف کنید؟</string>
|
||||
<string name="confirmation_remove_folders_alert">آیا واقعاً میخواهید موارد انتخاب شده و محتوای آنها حذف شود؟</string>
|
||||
<string name="confirmation_remove_local">فقط محلی</string>
|
||||
<string name="conflict_file_headline">فایل متناقض %1$s</string>
|
||||
<string name="conflict_local_file">پروندهٔ محلّی</string>
|
||||
<string name="conflict_message_description">اگر هردو نسخه را انتخاب کنید، یک شماره به نام فایل محلی اضافه خواهد شد.</string>
|
||||
<string name="conflict_server_file">پروندهٔ کارساز</string>
|
||||
|
|
|
@ -158,7 +158,6 @@
|
|||
<string name="confirmation_remove_folder_alert">Haluatko varmasti poistaa kohteen %1$s ja sen sisällön?</string>
|
||||
<string name="confirmation_remove_folders_alert">Haluatko varmasti poistaa valitut kohteet ja niiden sisällön?</string>
|
||||
<string name="confirmation_remove_local">Vain paikallisen</string>
|
||||
<string name="conflict_file_headline">Ristiriitainen kohde %1$s</string>
|
||||
<string name="conflict_local_file">Paikallinen tiedosto</string>
|
||||
<string name="conflict_message_description">Jos valitset molemmat versiot, paikallisen tiedoston nimeen lisätään numero.</string>
|
||||
<string name="conflict_server_file">Palvelintiedosto</string>
|
||||
|
|
|
@ -179,7 +179,6 @@
|
|||
<string name="confirmation_remove_folders_alert">Souhaitez-vous vraiment supprimer les éléments sélectionnés ainsi que leurs contenus ?</string>
|
||||
<string name="confirmation_remove_local">Local seulement</string>
|
||||
<string name="conflict_dialog_error">Erreur lors de la création de la boîte de dialogue de conflit !</string>
|
||||
<string name="conflict_file_headline">Fichier %1$s en conflit</string>
|
||||
<string name="conflict_local_file">fichier local</string>
|
||||
<string name="conflict_message_description">Si vous sélectionnez les deux versions, le fichier local aura un numéro ajouté à son nom.</string>
|
||||
<string name="conflict_server_file">Fichier serveur</string>
|
||||
|
|
|
@ -179,7 +179,6 @@
|
|||
<string name="confirmation_remove_folders_alert">An bhfuil tú cinnte gur mhaith leat na míreanna roghnaithe agus a bhfuil iontu a scriosadh?</string>
|
||||
<string name="confirmation_remove_local">Áitiúil amháin</string>
|
||||
<string name="conflict_dialog_error">Ní féidir dialóg réititheora coinbhleachta a chruthú</string>
|
||||
<string name="conflict_file_headline">Comhad contrártha %1$s</string>
|
||||
<string name="conflict_local_file">Comhad áitiúil</string>
|
||||
<string name="conflict_message_description">Má roghnaíonn tú an dá leagan, beidh uimhir ag gabháil leis an gcomhad áitiúil lena ainm.</string>
|
||||
<string name="conflict_server_file">Comhad freastalaí</string>
|
||||
|
|
|
@ -137,7 +137,6 @@
|
|||
<string name="confirmation_remove_folder_alert">A bheil thu cinnteach gu bheil thu airson %1$s ’s a shusbaint a sguabadh às?</string>
|
||||
<string name="confirmation_remove_folders_alert">A bheil thu cinnteach gu bheil thu airson na nithean a thagh thu ’s an susbaint a sguabadh às?</string>
|
||||
<string name="confirmation_remove_local">Ionadail a-mhàin</string>
|
||||
<string name="conflict_file_headline">Faidhle %1$s ann an còmhstri</string>
|
||||
<string name="conflict_message_description">Ma thaghas tu an dà thionndadh, thèid àireamh a chur ri ainm an fhaidhle ionadail.</string>
|
||||
<string name="contactlist_item_icon">Ìomhaigheag a’ chleachdaiche air liosta an luchd-aithne</string>
|
||||
<string name="contactlist_no_permission">Cha deach cead a thoirt ’s cha deach càil ion-phortadh.</string>
|
||||
|
|
|
@ -179,7 +179,6 @@
|
|||
<string name="confirmation_remove_folders_alert">Confirma que quere eliminar os elementos seleccionados e o seu contido?</string>
|
||||
<string name="confirmation_remove_local">Só local</string>
|
||||
<string name="conflict_dialog_error">Non é posíbel crear o diálogo de resolución de conflitos</string>
|
||||
<string name="conflict_file_headline">Ficheiro en conflito %1$s</string>
|
||||
<string name="conflict_local_file">Ficheiro local</string>
|
||||
<string name="conflict_message_description">Se selecciona ambas versións, o ficheiro local terá un número engadido ao nome.</string>
|
||||
<string name="conflict_server_file">Ficheiro do servidor</string>
|
||||
|
@ -690,7 +689,7 @@
|
|||
<string name="preview_image_error_unknown_format">Non é posíbel amosar a imaxe</string>
|
||||
<string name="preview_image_file_is_not_downloaded">O ficheiro non está descargado</string>
|
||||
<string name="preview_image_file_is_not_exist">O ficheiro non existe</string>
|
||||
<string name="preview_media_unhandled_http_code_message">O ficheiro está bloqueado actualmente por outro usuario ou proceso e, polo tanto, non é posíbel eliminalo. Ténteo de novo máis tarde.</string>
|
||||
<string name="preview_media_unhandled_http_code_message">O ficheiro está bloqueado actualmente por outro usuario ou proceso e, por tanto, non é posíbel eliminalo. Ténteo de novo máis tarde.</string>
|
||||
<string name="preview_sorry">Desculpe.</string>
|
||||
<string name="privacy">Privacidade</string>
|
||||
<string name="public_share_name">Nome novo</string>
|
||||
|
|
|
@ -145,7 +145,6 @@
|
|||
<string name="confirmation_remove_folder_alert">Želite li zaista izbrisati %1$s i pripadajući sadržaj?</string>
|
||||
<string name="confirmation_remove_folders_alert">Želite li zaista izbrisati odabrane stavke i pripadajući sadržaj?</string>
|
||||
<string name="confirmation_remove_local">Samo lokalno</string>
|
||||
<string name="conflict_file_headline">Nepodudarna datoteka %1$s</string>
|
||||
<string name="conflict_local_file">Lokalna datoteka</string>
|
||||
<string name="conflict_message_description">Ako odaberete obje inačice, lokalna će datoteka uz naziv imati i broj.</string>
|
||||
<string name="conflict_server_file">Datoteka na poslužitelju</string>
|
||||
|
|
|
@ -155,7 +155,6 @@
|
|||
<string name="confirmation_remove_folder_alert">Biztos, hogy törli ezt: %1$s és a tartalmát?</string>
|
||||
<string name="confirmation_remove_folders_alert">Biztos, hogy törli a kiválasztott elemeket és tartalmukat?</string>
|
||||
<string name="confirmation_remove_local">Csak a helyi példány</string>
|
||||
<string name="conflict_file_headline">Ütköző fájl: %1$s</string>
|
||||
<string name="conflict_local_file">Helyi fájl</string>
|
||||
<string name="conflict_message_description">Amennyiben mindkét verziót kiválasztja, a helyi fájl nevéhez egy szám lesz hozzáfűzve.</string>
|
||||
<string name="conflict_server_file">Kiszolgálón lévő fájl</string>
|
||||
|
|
|
@ -173,7 +173,6 @@ Otomatis unggah hanya bekerja dengan baik apabila Anda mengeluarkan aplikasi ini
|
|||
<string name="confirmation_remove_folders_alert">Apa anda yakin ingin menghapus item yang terpilih beserta isinya?</string>
|
||||
<string name="confirmation_remove_local">Lokal saja</string>
|
||||
<string name="conflict_dialog_error">Dialog penyelesaian konflik tidak dapat dibuat</string>
|
||||
<string name="conflict_file_headline">File konflik %1$s</string>
|
||||
<string name="conflict_local_file">File lokal</string>
|
||||
<string name="conflict_message_description">Jika Anda memilih kedua versi, nama dari berkas lokal akan ditambahi angka.</string>
|
||||
<string name="conflict_server_file">File server</string>
|
||||
|
|
|
@ -169,7 +169,6 @@
|
|||
<string name="confirmation_remove_folder_alert">Vuoi davvero rimuovere %1$s e il relativo contenuto?</string>
|
||||
<string name="confirmation_remove_folders_alert">Vuoi davvero eliminare gli elementi selezionati e il loro contenuto?</string>
|
||||
<string name="confirmation_remove_local">Solo localmente</string>
|
||||
<string name="conflict_file_headline">File %1$s in conflitto</string>
|
||||
<string name="conflict_local_file">File locale</string>
|
||||
<string name="conflict_message_description">Se selezioni entrambe le versioni, il file locale ha un numero aggiunto al suo nome.</string>
|
||||
<string name="conflict_server_file">File su server</string>
|
||||
|
|
|
@ -179,7 +179,6 @@
|
|||
<string name="confirmation_remove_folders_alert">本当に選択したアイテムとその内容を削除しますか?</string>
|
||||
<string name="confirmation_remove_local">ローカルのみ</string>
|
||||
<string name="conflict_dialog_error">競合解決ダイアログを作成できません</string>
|
||||
<string name="conflict_file_headline">%1$sはすでに存在します</string>
|
||||
<string name="conflict_local_file">ローカルファイル</string>
|
||||
<string name="conflict_message_description">両方のバージョンを選択した場合、ローカルファイルはファイル名に数字が追加されます。</string>
|
||||
<string name="conflict_server_file">サーバーファイル</string>
|
||||
|
|
|
@ -151,7 +151,6 @@
|
|||
<string name="confirmation_remove_folder_alert">Do you really want to delete %1$s and the contents thereof?</string>
|
||||
<string name="confirmation_remove_folders_alert">Do you really want to delete the selected items and their contents?</string>
|
||||
<string name="confirmation_remove_local">Local only</string>
|
||||
<string name="conflict_file_headline">Conflicting file %1$s</string>
|
||||
<string name="conflict_local_file">Local file</string>
|
||||
<string name="conflict_message_description">If you select both versions, the local file will have a number appended to its name.</string>
|
||||
<string name="conflict_server_file">Server file</string>
|
||||
|
|
|
@ -178,7 +178,6 @@
|
|||
<string name="confirmation_remove_folders_alert">선택한 항목과 포함된 내용을 삭제하시겠습니까?</string>
|
||||
<string name="confirmation_remove_local">로컬만</string>
|
||||
<string name="conflict_dialog_error">충돌 해결 프로그램 대화 상자를 만들 수 없습니다.</string>
|
||||
<string name="conflict_file_headline">충돌하는 파일 %1$s</string>
|
||||
<string name="conflict_local_file">로컬 파일</string>
|
||||
<string name="conflict_message_description">두 버전을 모두 선택하면 기존 파일 이름에 번호가 추가됩니다.</string>
|
||||
<string name="conflict_server_file">서버 파일</string>
|
||||
|
|
|
@ -144,7 +144,6 @@
|
|||
<string name="confirmation_remove_folder_alert">ທ່ານຕ້ອງການ ລຶບ%1$s ແລະ ເນື້ອຫາບໍ?</string>
|
||||
<string name="confirmation_remove_folders_alert">ທ່ານຕ້ອງການລຶບລາຍການທີ່ເລືອກ ແລະ ເນື້ອຫາແທ້ບໍ?</string>
|
||||
<string name="confirmation_remove_local">ຊ່ອງເກັບຢ່າງດຽວ</string>
|
||||
<string name="conflict_file_headline">ຟາຍຜິດພາດ%1$s</string>
|
||||
<string name="conflict_message_description">ຖ້າທ່ານເລືອກເອົາທັງສອງເວີຊັ້ນ, ບ່ອນເກັບຟາຍຈະມີຈໍານວນສະສົມ</string>
|
||||
<string name="contactlist_item_icon">ລາຍການໄອຄອນຜູ້ຕິດຕໍ່</string>
|
||||
<string name="contactlist_no_permission">ບໍ່ໄດ້ຮັບອະນຸຍາດ, ບໍ່ມີຫຍັງນໍາເຂົ້າ.</string>
|
||||
|
|
|
@ -147,7 +147,6 @@
|
|||
<string name="confirmation_remove_folder_alert">Ar tikrai norite ištrinti %1$s ir jo turinį?</string>
|
||||
<string name="confirmation_remove_folders_alert">Ar tikrai norite ištrinti pažymėtus elementus ir jų turinį?</string>
|
||||
<string name="confirmation_remove_local">Tik vietiniai</string>
|
||||
<string name="conflict_file_headline">Nesuderinamas failas%1$s</string>
|
||||
<string name="conflict_local_file">Vietinis failas</string>
|
||||
<string name="conflict_message_description">Jei pasirinksite abi versijas, vietinis failas prie pavadinimo turės numerį.</string>
|
||||
<string name="conflict_server_file">Failas iš serverio</string>
|
||||
|
|
|
@ -132,7 +132,6 @@
|
|||
<string name="confirmation_remove_folder_alert">Vai tiešām vēlaties izdzēst %1$s un tā saturu?</string>
|
||||
<string name="confirmation_remove_folders_alert">Vai tiešām vēlies dzēst izvēlētos objektus un to saturu?</string>
|
||||
<string name="confirmation_remove_local">Tikai lokālos</string>
|
||||
<string name="conflict_file_headline">Konfliktējošs fails %1$s</string>
|
||||
<string name="contactlist_item_icon">Lietotāja ikona kontaktpersonu sarakstam</string>
|
||||
<string name="contactlist_no_permission">Nav dota atļauja, importēšana neizdevās</string>
|
||||
<string name="contacts">Kontakti</string>
|
||||
|
|
|
@ -144,7 +144,6 @@
|
|||
<string name="confirmation_remove_folder_alert">али си сигурен дека сакаш да ја избришеш %1$s и содржината во истата?</string>
|
||||
<string name="confirmation_remove_folders_alert">Дали си сигурен дека сакаш да ја избришеш означената ставкаи содржината во неа?</string>
|
||||
<string name="confirmation_remove_local">Само локално</string>
|
||||
<string name="conflict_file_headline">Датотеки со конфликт %1$s</string>
|
||||
<string name="conflict_local_file">Локална датотека</string>
|
||||
<string name="conflict_message_description">Ако ги одберете и двете верзии, локалната датотека ќе има број додаден на нејзиното име.</string>
|
||||
<string name="conflict_server_file">Серверска датотека</string>
|
||||
|
|
|
@ -179,7 +179,6 @@
|
|||
<string name="confirmation_remove_folders_alert">Vil du virkelig fjerne de valgte elementene og dets innhold?</string>
|
||||
<string name="confirmation_remove_local">Kun lokalt</string>
|
||||
<string name="conflict_dialog_error">Dialogboksen Konfliktløser kan ikke opprettes</string>
|
||||
<string name="conflict_file_headline">Konflikt med%1$s</string>
|
||||
<string name="conflict_local_file">Lokal fil</string>
|
||||
<string name="conflict_message_description">Hvis du velger begge versjonene vil den lokale filen få et tall lagt til på slutten av navnet.</string>
|
||||
<string name="conflict_server_file">Server fil</string>
|
||||
|
@ -487,6 +486,8 @@
|
|||
<string name="instant_upload_existing">Last også opp eksisterende filer</string>
|
||||
<string name="instant_upload_on_charging">Bare last opp under lading</string>
|
||||
<string name="instant_upload_path">/Direkteopplasting</string>
|
||||
<string name="internal_two_way_sync">Intern toveis synkronisering</string>
|
||||
<string name="internal_two_way_sync_not_yet">Ikke enda, snart synkronisert</string>
|
||||
<string name="invalid_url">Ugyldig URL</string>
|
||||
<string name="invisible">Usynlig</string>
|
||||
<string name="label_empty">Merkelapp kan ikke være tom</string>
|
||||
|
@ -677,6 +678,7 @@
|
|||
<string name="prefs_synced_folders_local_path_title">Lokal mappe</string>
|
||||
<string name="prefs_synced_folders_remote_path_title">Mappe på server</string>
|
||||
<string name="prefs_theme_title">Tema</string>
|
||||
<string name="prefs_two_way_sync_summary">Administrer interne mapper for toveis synkronisering</string>
|
||||
<string name="prefs_value_theme_dark">Mørk</string>
|
||||
<string name="prefs_value_theme_light">Lys</string>
|
||||
<string name="prefs_value_theme_system">Følg system</string>
|
||||
|
|
|
@ -178,7 +178,6 @@
|
|||
<string name="confirmation_remove_folders_alert">Wil je de geselecteerde objecten en hun inhoud echt verwijderen?</string>
|
||||
<string name="confirmation_remove_local">Alleen lokaal</string>
|
||||
<string name="conflict_dialog_error">Conflictoplossingsvenster kan niet geladen worden</string>
|
||||
<string name="conflict_file_headline">Conflicterend bestand%1$s</string>
|
||||
<string name="conflict_local_file">Lokaal bestand</string>
|
||||
<string name="conflict_message_description">Als je beide versies selecteert, zal het lokale bestand een nummer aan de naam toegevoegd krijgen.</string>
|
||||
<string name="conflict_server_file">Serverbestand</string>
|
||||
|
|
|
@ -176,7 +176,6 @@
|
|||
<string name="confirmation_remove_folders_alert">Czy na pewno chcesz usunąć wybrane pozycje i ich zawartość?</string>
|
||||
<string name="confirmation_remove_local">Tylko lokalnie</string>
|
||||
<string name="conflict_dialog_error">Nie można utworzyć okna dialogowego rozwiązywania konfliktów</string>
|
||||
<string name="conflict_file_headline">Plik powodujący konflikt %1$s</string>
|
||||
<string name="conflict_local_file">Plik lokalny</string>
|
||||
<string name="conflict_message_description">Jeśli wybierzesz obie wersje, to do nazwy pliku lokalnego zostanie dodany numer.</string>
|
||||
<string name="conflict_server_file">Plik z serwera</string>
|
||||
|
|
|
@ -179,7 +179,6 @@
|
|||
<string name="confirmation_remove_folders_alert">Quer realmente excluir os itens selecionados e seus conteúdos?</string>
|
||||
<string name="confirmation_remove_local">Somente local</string>
|
||||
<string name="conflict_dialog_error">A caixa de diálogo de resolução de conflitos não pode ser criada</string>
|
||||
<string name="conflict_file_headline">Arquivo conflitante %1$s</string>
|
||||
<string name="conflict_local_file">Arquivo local</string>
|
||||
<string name="conflict_message_description">Se você selecionar as duas versões, o arquivo local terá um número anexado ao seu nome.</string>
|
||||
<string name="conflict_server_file">Arquivo do servidor</string>
|
||||
|
@ -279,8 +278,11 @@
|
|||
<string name="drawer_quota">%1$s de %2$s usados</string>
|
||||
<string name="drawer_quota_unlimited">%1$s usados</string>
|
||||
<string name="drawer_synced_folders">Autoenvio</string>
|
||||
<string name="e2e_counter_too_old">O contador é muito antigo</string>
|
||||
<string name="e2e_hash_not_found">Hash não encontrado</string>
|
||||
<string name="e2e_not_yet_setup">E2E ainda não configurado</string>
|
||||
<string name="e2e_offline">Não é possível sem conexão com a internet</string>
|
||||
<string name="e2e_signature_does_not_match">A assinatura não corresponde</string>
|
||||
<string name="ecosystem_apps_display_assistant">Assistente</string>
|
||||
<string name="ecosystem_apps_display_more">Mais</string>
|
||||
<string name="ecosystem_apps_display_notes">Notas </string>
|
||||
|
@ -406,6 +408,14 @@
|
|||
<string name="file_migration_updating_index">Atualizando índice…</string>
|
||||
<string name="file_migration_use_data_folder">Usar</string>
|
||||
<string name="file_migration_waiting_for_unfinished_sync">Aguardando sincronização completa…</string>
|
||||
<string name="file_name_validator_current_path_is_invalid">O nome da pasta atual é inválido, renomeie a pasta. Redirecionando para a raiz</string>
|
||||
<string name="file_name_validator_error_contains_reserved_names_or_invalid_characters">O caminho da pasta contém nomes reservados ou caracteres inválidos</string>
|
||||
<string name="file_name_validator_error_ends_with_space_period">O nome termina com um espaço ou um ponto</string>
|
||||
<string name="file_name_validator_error_forbidden_file_extensions">.%s é uma extensão de arquivo proibida</string>
|
||||
<string name="file_name_validator_error_invalid_character">O nome contém um caractere inválido:%s</string>
|
||||
<string name="file_name_validator_error_reserved_names">%s é um nome proibido</string>
|
||||
<string name="file_name_validator_rename_before_move_or_copy">%s. Renomeie o arquivo antes de mover ou copiar</string>
|
||||
<string name="file_name_validator_upload_content_error">Alguns conteúdos não podem ser carregados porque contêm nomes reservados ou caracteres inválidos</string>
|
||||
<string name="file_not_found">Arquivo não encontrado</string>
|
||||
<string name="file_not_synced">Arquivo não pôde ser sincronizado. Mostrando a última versão disponível.</string>
|
||||
<string name="file_rename">Renomear</string>
|
||||
|
@ -429,6 +439,7 @@
|
|||
<string name="folder_already_exists">Pasta já existe</string>
|
||||
<string name="folder_confirm_create">Criar</string>
|
||||
<string name="folder_list_empty_headline">Sem pastas aqui</string>
|
||||
<string name="folder_name_empty">O nome da pasta não pode ficar vazio</string>
|
||||
<string name="folder_picker_choose_button_text">Escolher</string>
|
||||
<string name="folder_picker_choose_caption_text">Escolher pasta destino</string>
|
||||
<string name="folder_picker_copy_button_text">Copiar</string>
|
||||
|
@ -475,6 +486,8 @@
|
|||
<string name="instant_upload_existing">Enviar arquivos existentes também</string>
|
||||
<string name="instant_upload_on_charging">Só enviar quando carregando</string>
|
||||
<string name="instant_upload_path">/EnvioAutomático</string>
|
||||
<string name="internal_two_way_sync">Sincronização interna bidirecional</string>
|
||||
<string name="internal_two_way_sync_not_yet">Ainda não, em breve será sincronizado</string>
|
||||
<string name="invalid_url">URL inválida</string>
|
||||
<string name="invisible">Invisível</string>
|
||||
<string name="label_empty">A etiqueta não pode ficar vazia</string>
|
||||
|
@ -665,6 +678,7 @@
|
|||
<string name="prefs_synced_folders_local_path_title">Pasta local</string>
|
||||
<string name="prefs_synced_folders_remote_path_title">Pasta remota</string>
|
||||
<string name="prefs_theme_title">Tema</string>
|
||||
<string name="prefs_two_way_sync_summary">Gerencie pastas internas para sincronização bidirecional</string>
|
||||
<string name="prefs_value_theme_dark">Escuro</string>
|
||||
<string name="prefs_value_theme_light">Claro</string>
|
||||
<string name="prefs_value_theme_system">Seguir o sistema</string>
|
||||
|
|
|
@ -149,7 +149,6 @@
|
|||
<string name="confirmation_remove_folder_alert">Deseja realmente apagar %1$s e o seu conteúdo?</string>
|
||||
<string name="confirmation_remove_folders_alert">Quer realmente apagar os itens seleccionados e os seus conteúdos?</string>
|
||||
<string name="confirmation_remove_local">Apenas localmente</string>
|
||||
<string name="conflict_file_headline">Ficheiro em conflito %1$s</string>
|
||||
<string name="conflict_local_file">Ficheiro local</string>
|
||||
<string name="conflict_message_description">Se selecionou ambas as versões, o ficheiro local terá um número acrescentado ao seu nome.</string>
|
||||
<string name="conflict_server_file">Ficheiro do servidor</string>
|
||||
|
|
|
@ -174,7 +174,6 @@
|
|||
<string name="confirmation_remove_folder_alert">Sigur vreți să eliminați %1$s și conținutul său?</string>
|
||||
<string name="confirmation_remove_folders_alert">Doriți să ștergeți elementele selectate și conținutul lor?</string>
|
||||
<string name="confirmation_remove_local">Doar local</string>
|
||||
<string name="conflict_file_headline">Conflict cu fișierul %1$s</string>
|
||||
<string name="conflict_local_file">Fișier local</string>
|
||||
<string name="conflict_message_description">Dacă selectezi ambele variante, atunci fișierul local va avea un număr adăugat la numele său.</string>
|
||||
<string name="conflict_server_file">Fișier pe server</string>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue