Internal two way sync

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
tobiasKaminsky 2024-07-16 09:06:52 +02:00 committed by Alper Öztürk
parent 24eb7f02e8
commit 82c6956566
33 changed files with 1789 additions and 29 deletions

View file

@ -1206,4 +1206,4 @@
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '082a63031678a67879428f688f02d3b5')"
]
}
}
}

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -106,7 +106,6 @@ public class FileIT extends AbstractOnServerIT {
assertTrue(new SynchronizeFolderOperation(targetContext,
folderPath,
user,
System.currentTimeMillis(),
fileDataStorageManager)
.execute(targetContext)
.isSuccess());

View file

@ -255,6 +255,9 @@
<activity
android:name=".ui.activity.SyncedFoldersActivity"
android:exported="false" />
<activity
android:name=".ui.activity.InternalTwoWaySyncActivity"
android:exported="false" />
<activity
android:name="com.nextcloud.client.widget.DashboardWidgetConfigurationActivity"
android:exported="false">
@ -627,4 +630,4 @@
</activity>
</application>
</manifest>
</manifest>

View file

@ -60,7 +60,8 @@ import com.owncloud.android.db.ProviderMeta
AutoMigration(from = 78, to = 79, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 79, to = 80),
AutoMigration(from = 80, to = 81),
AutoMigration(from = 81, to = 82)
AutoMigration(from = 81, to = 82),
AutoMigration(from = 82, to = 83)
],
exportSchema = true
)

View file

@ -49,4 +49,10 @@ interface FileDao {
@Query("SELECT * FROM filelist where file_owner = :fileOwner AND etag_in_conflict IS NOT NULL")
fun getFilesWithSyncConflict(fileOwner: String): List<FileEntity>
@Query(
"SELECT * FROM filelist where file_owner = :fileOwner AND internal_two_way_sync_timestamp >= 0 " +
"ORDER BY internal_two_way_sync_timestamp DESC"
)
fun getInternalTwoWaySyncFolders(fileOwner: String): List<FileEntity>
}

View file

@ -115,5 +115,9 @@ data class FileEntity(
@ColumnInfo(name = ProviderTableMeta.FILE_METADATA_GPS)
val metadataGPS: String?,
@ColumnInfo(name = ProviderTableMeta.FILE_E2E_COUNTER)
val e2eCounter: Long?
val e2eCounter: Long?,
@ColumnInfo(name = ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP)
val internalTwoWaySync: Long?,
@ColumnInfo(name = ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT)
val internalTwoWaySyncResult: String?
)

View file

@ -54,6 +54,7 @@ import com.owncloud.android.ui.activity.FileActivity;
import com.owncloud.android.ui.activity.FileDisplayActivity;
import com.owncloud.android.ui.activity.FilePickerActivity;
import com.owncloud.android.ui.activity.FolderPickerActivity;
import com.owncloud.android.ui.activity.InternalTwoWaySyncActivity;
import com.owncloud.android.ui.activity.ManageAccountsActivity;
import com.owncloud.android.ui.activity.ManageSpaceActivity;
import com.owncloud.android.ui.activity.NotificationsActivity;
@ -476,4 +477,7 @@ abstract class ComponentsModule {
@ContributesAndroidInjector
abstract TestJob testJob();
@ContributesAndroidInjector
abstract InternalTwoWaySyncActivity internalTwoWaySyncActivity();
}

View file

@ -95,6 +95,7 @@ class BackgroundJobFactory @Inject constructor(
GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters)
HealthStatusWork::class -> createHealthStatusWork(context, workerParameters)
TestJob::class -> createTestJob(context, workerParameters)
InternalTwoWaySyncWork::class -> createInternalTwoWaySyncWork(context, workerParameters)
else -> null // caller falls back to default factory
}
}
@ -277,4 +278,14 @@ class BackgroundJobFactory @Inject constructor(
backgroundJobManager.get()
)
}
private fun createInternalTwoWaySyncWork(context: Context, params: WorkerParameters): InternalTwoWaySyncWork {
return InternalTwoWaySyncWork(
context,
params,
accountManager,
powerManagementService,
connectivityService
)
}
}

View file

@ -168,4 +168,5 @@ interface BackgroundJobManager {
fun schedulePeriodicHealthStatus()
fun startHealthStatus()
fun bothFilesSyncJobsRunning(syncedFolderID: Long): Boolean
fun scheduleInternal2WaySync()
}

View file

@ -84,6 +84,8 @@ internal class BackgroundJobManagerImpl(
const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status"
const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status"
const val JOB_INTERNAL_TWO_WAY_SYNC = "internal_two_way_sync"
const val JOB_TEST = "test_job"
const val MAX_CONTENT_TRIGGER_DELAY_MS = 10000L
@ -647,4 +649,13 @@ internal class BackgroundJobManagerImpl(
request
)
}
override fun scheduleInternal2WaySync() {
val request = periodicRequestBuilder(
jobClass = InternalTwoWaySyncWork::class,
jobName = JOB_INTERNAL_TWO_WAY_SYNC
).build()
workManager.enqueueUniquePeriodicWork(JOB_INTERNAL_TWO_WAY_SYNC, ExistingPeriodicWorkPolicy.KEEP, request)
}
}

View file

@ -0,0 +1,91 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.jobs
import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.device.PowerManagementService
import com.nextcloud.client.network.ConnectivityService
import com.owncloud.android.MainApp
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.operations.SynchronizeFolderOperation
import com.owncloud.android.utils.FileStorageUtils
import java.io.File
@Suppress("Detekt.NestedBlockDepth")
class InternalTwoWaySyncWork(
private val context: Context,
params: WorkerParameters,
private val userAccountManager: UserAccountManager,
private val powerManagementService: PowerManagementService,
private val connectivityService: ConnectivityService
) : Worker(context, params) {
override fun doWork(): Result {
Log_OC.d(TAG, "Worker started!")
var result = true
if (powerManagementService.isPowerSavingEnabled ||
!connectivityService.isConnected || connectivityService.isInternetWalled
) {
Log_OC.d(TAG, "Not starting due to constraints!")
return Result.success()
}
val users = userAccountManager.allUsers
for (user in users) {
val fileDataStorageManager = FileDataStorageManager(user, context.contentResolver)
val folders = fileDataStorageManager.getInternalTwoWaySyncFolders(user)
for (folder in folders) {
val freeSpaceLeft = File(folder.storagePath).getFreeSpace()
val localFolderSize = FileStorageUtils.getFolderSize(File(folder.storagePath, MainApp.getDataFolder()))
val remoteFolderSize = folder.fileLength
if (freeSpaceLeft < (remoteFolderSize - localFolderSize)) {
Log_OC.d(TAG, "Not enough space left!")
result = false
}
Log_OC.d(TAG, "Folder ${folder.remotePath}: started!")
val operation = SynchronizeFolderOperation(context, folder.remotePath, user, fileDataStorageManager)
.execute(context)
if (operation.isSuccess) {
Log_OC.d(TAG, "Folder ${folder.remotePath}: finished!")
} else {
Log_OC.d(TAG, "Folder ${folder.remotePath} failed!")
result = false
}
folder.apply {
internalFolderSyncResult = operation.code.toString()
internalFolderSyncTimestamp = System.currentTimeMillis()
}
fileDataStorageManager.saveFile(folder)
}
}
return if (result) {
Log_OC.d(TAG, "Worker finished with success!")
Result.success()
} else {
Log_OC.d(TAG, "Worker finished with failure!")
Result.failure()
}
}
companion object {
const val TAG = "InternalTwoWaySyncWork"
}
}

View file

@ -371,6 +371,7 @@ public class MainApp extends Application implements HasAndroidInjector {
backgroundJobManager.scheduleMediaFoldersDetectionJob();
backgroundJobManager.startMediaFoldersDetectionJob();
backgroundJobManager.schedulePeriodicHealthStatus();
backgroundJobManager.scheduleInternal2WaySync();
}
registerGlobalPassCodeProtection();

View file

@ -77,6 +77,7 @@ import androidx.annotation.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import kotlin.Pair;
@SuppressFBWarnings("CE")
public class FileDataStorageManager {
private static final String TAG = FileDataStorageManager.class.getSimpleName();
@ -558,6 +559,8 @@ public class FileDataStorageManager {
cv.put(ProviderTableMeta.FILE_SHAREES, gson.toJson(fileOrFolder.getSharees()));
cv.put(ProviderTableMeta.FILE_TAGS, gson.toJson(fileOrFolder.getTags()));
cv.put(ProviderTableMeta.FILE_RICH_WORKSPACE, fileOrFolder.getRichWorkspace());
cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP, fileOrFolder.getInternalFolderSyncTimestamp());
cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT, fileOrFolder.getInternalFolderSyncResult());
return cv;
}
@ -602,6 +605,8 @@ public class FileDataStorageManager {
cv.put(ProviderTableMeta.FILE_METADATA_GPS, gson.toJson(file.getGeoLocation()));
cv.put(ProviderTableMeta.FILE_METADATA_LIVE_PHOTO, file.getLinkedFileIdForLivePhoto());
cv.put(ProviderTableMeta.FILE_E2E_COUNTER, file.getE2eCounter());
cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP, file.getInternalFolderSyncTimestamp());
cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT, file.getInternalFolderSyncResult());
return cv;
}
@ -1035,6 +1040,7 @@ public class FileDataStorageManager {
ocFile.setLivePhoto(fileEntity.getMetadataLivePhoto());
ocFile.setHidden(nullToZero(fileEntity.getHidden()) == 1);
ocFile.setE2eCounter(fileEntity.getE2eCounter());
ocFile.setInternalFolderSyncTimestamp(fileEntity.getInternalTwoWaySync());
String sharees = fileEntity.getSharees();
// Surprisingly JSON deserialization causes significant overhead.
@ -2477,4 +2483,29 @@ 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());
for (FileEntity fileEntity : fileEntities) {
files.add(createFileInstance(fileEntity));
}
return files;
}
public boolean isPartOfInternalTwoWaySync(OCFile file) {
if (file.isInternalFolderSync()) {
return true;
}
while (file != null && !OCFile.ROOT_PATH.equals(file.getDecryptedRemotePath())) {
if (file.isInternalFolderSync()) {
return true;
}
file = getFileById(file.getParentId());
}
return false;
}
}

View file

@ -117,6 +117,8 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
@Nullable
private GeoLocation geolocation;
private List<String> tags = new ArrayList<>();
private Long internalFolderSyncTimestamp = -1L;
private String internalFolderSyncResult = "";
/**
* URI to the local path of the file contents, if stored in the device; cached after first call to
@ -1051,6 +1053,26 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
this.e2eCounter = e2eCounter;
}
}
public boolean isInternalFolderSync() {
return internalFolderSyncTimestamp >= 0;
}
public Long getInternalFolderSyncTimestamp() {
return internalFolderSyncTimestamp;
}
public void setInternalFolderSyncTimestamp(Long internalFolderSyncTimestamp) {
this.internalFolderSyncTimestamp = internalFolderSyncTimestamp;
}
public String getInternalFolderSyncResult() {
return internalFolderSyncResult;
}
public void setInternalFolderSyncResult(String internalFolderSyncResult) {
this.internalFolderSyncResult = internalFolderSyncResult;
}
public boolean isAPKorAAB() {
if ("gplay".equals(BuildConfig.FLAVOR)) {

View file

@ -25,7 +25,7 @@ import java.util.List;
*/
public class ProviderMeta {
public static final String DB_NAME = "filelist";
public static final int DB_VERSION = 82;
public static final int DB_VERSION = 83;
private ProviderMeta() {
// No instance
@ -120,6 +120,8 @@ public class ProviderMeta {
public static final String FILE_LOCK_TOKEN = "lock_token";
public static final String FILE_TAGS = "tags";
public static final String FILE_E2E_COUNTER = "e2e_counter";
public static final String FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP = "internal_two_way_sync_timestamp";
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,
@ -171,7 +173,9 @@ public class ProviderMeta {
FILE_METADATA_LIVE_PHOTO,
FILE_E2E_COUNTER,
FILE_TAGS,
FILE_METADATA_GPS));
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

View file

@ -697,6 +697,7 @@ public class RefreshFolderOperation extends RemoteOperation {
if (localFile != null) {
updatedFile.setFileId(localFile.getFileId());
updatedFile.setLastSyncDateForData(localFile.getLastSyncDateForData());
updatedFile.setInternalFolderSyncTimestamp(localFile.getInternalFolderSyncTimestamp());
updatedFile.setModificationTimestampAtLastSyncForData(
localFile.getModificationTimestampAtLastSyncForData()
);

View file

@ -295,6 +295,8 @@ public class SynchronizeFileOperation extends SyncOperation {
}
private void requestForDownload(OCFile file) {
Log_OC.d("InternalTwoWaySyncWork", "download file: " + file.getFileName());
FileDownloadHelper.Companion.instance().downloadFile(
mUser,
file);

View file

@ -55,9 +55,6 @@ public class SynchronizeFolderOperation extends SyncOperation {
private static final String TAG = SynchronizeFolderOperation.class.getSimpleName();
/** Time stamp for the synchronization process in progress */
private long mCurrentSyncTime;
/** Remote path of the folder to synchronize */
private String mRemotePath;
@ -95,17 +92,14 @@ public class SynchronizeFolderOperation extends SyncOperation {
* @param context Application context.
* @param remotePath Path to synchronize.
* @param user Nextcloud account where the folder is located.
* @param currentSyncTime Time stamp for the synchronization process in progress.
*/
public SynchronizeFolderOperation(Context context,
String remotePath,
User user,
long currentSyncTime,
FileDataStorageManager storageManager) {
super(storageManager);
mRemotePath = remotePath;
mCurrentSyncTime = currentSyncTime;
this.user = user;
mContext = context;
mRemoteFolderChanged = false;
@ -365,7 +359,7 @@ public class SynchronizeFolderOperation extends SyncOperation {
}
private void updateLocalStateData(OCFile remoteFile, OCFile localFile, OCFile updatedFile) {
updatedFile.setLastSyncDateForProperties(mCurrentSyncTime);
updatedFile.setLastSyncDateForProperties(System.currentTimeMillis());
if (localFile != null) {
updatedFile.setFileId(localFile.getFileId());
updatedFile.setLastSyncDateForData(localFile.getLastSyncDateForData());
@ -393,8 +387,19 @@ public class SynchronizeFolderOperation extends SyncOperation {
}
}
private void classifyFileForLaterSyncOrDownload(OCFile remoteFile, OCFile localFile) {
if (!remoteFile.isFolder()) {
@SuppressFBWarnings("JLM")
private void classifyFileForLaterSyncOrDownload(OCFile remoteFile, OCFile localFile) throws OperationCancelledException {
if (remoteFile.isFolder()) {
/// to download children files recursively
synchronized (mCancellationRequested) {
if (mCancellationRequested.get()) {
throw new OperationCancelledException();
}
startSyncFolderOperation(remoteFile.getRemotePath());
}
} else {
/// prepare content synchronization for files (any file, not just favorites)
SynchronizeFileOperation operation = new SynchronizeFileOperation(
localFile,
remoteFile,

View file

@ -707,7 +707,6 @@ public class OperationsService extends Service {
this, // TODO remove this dependency from construction time
remotePath,
user,
System.currentTimeMillis(), // TODO remove this dependency from construction time
fileDataStorageManager
);
break;

View file

@ -0,0 +1,30 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.owncloud.android.ui.activity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import com.nextcloud.client.di.Injectable
import com.owncloud.android.databinding.InternalTwoWaySyncLayoutBinding
import com.owncloud.android.ui.adapter.InternalTwoWaySyncAdapter
class InternalTwoWaySyncActivity : BaseActivity(), Injectable {
lateinit var binding: InternalTwoWaySyncLayoutBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = InternalTwoWaySyncLayoutBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.list.apply {
adapter = InternalTwoWaySyncAdapter(fileDataStorageManager, user.get(), context)
layoutManager = LinearLayoutManager(context)
}
}
}

View file

@ -171,6 +171,9 @@ public class SettingsActivity extends PreferenceActivity
// Details
setupDetailsCategory(preferenceScreen);
// Sync
setupSyncCategory();
// More
setupMoreCategory();
@ -310,13 +313,19 @@ public class SettingsActivity extends PreferenceActivity
}
}
}
private void setupSyncCategory() {
final PreferenceCategory preferenceCategorySync = (PreferenceCategory) findPreference("sync");
viewThemeUtils.files.themePreferenceCategory(preferenceCategorySync);
setupAutoUploadPreference(preferenceCategorySync);
setupInternalTwoWaySyncPreference(preferenceCategorySync);
}
private void setupMoreCategory() {
final PreferenceCategory preferenceCategoryMore = (PreferenceCategory) findPreference("more");
viewThemeUtils.files.themePreferenceCategory(preferenceCategoryMore);
setupAutoUploadPreference(preferenceCategoryMore);
setupCalendarPreference(preferenceCategoryMore);
setupBackupPreference();
@ -548,6 +557,16 @@ public class SettingsActivity extends PreferenceActivity
});
}
}
private void setupInternalTwoWaySyncPreference(PreferenceCategory preferenceCategorySync) {
Preference twoWaySync = findPreference("internal_two_way_sync");
twoWaySync.setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(this, InternalTwoWaySyncActivity.class);
startActivity(intent);
return true;
});
}
private void setupBackupPreference() {
Preference pContactsBackup = findPreference("backup");

View file

@ -424,8 +424,12 @@ public class StorageMigration {
throw new MigrationException(R.string.file_migration_failed_dir_already_exists);
}
if (dstFile.getFreeSpace() < FileStorageUtils.getFolderSize(new File(srcFile, MainApp.getDataFolder()))) {
throw new MigrationException(R.string.file_migration_failed_not_enough_space);
try {
if (dstFile.getFreeSpace() < FileStorageUtils.getFolderSize(new File(srcFile, MainApp.getDataFolder()))) {
throw new MigrationException(R.string.file_migration_failed_not_enough_space);
}
} catch (MigrationException e) {
throw new RuntimeException(e);
}
}

View file

@ -0,0 +1,43 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.owncloud.android.ui.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.nextcloud.client.account.User
import com.owncloud.android.databinding.InternalTwoWaySyncViewHolderBinding
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
class InternalTwoWaySyncAdapter(
dataStorageManager: FileDataStorageManager,
user: User,
val context: Context
) : RecyclerView.Adapter<InternalTwoWaySyncViewHolder>() {
var folders: List<OCFile> = dataStorageManager.getInternalTwoWaySyncFolders(user)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InternalTwoWaySyncViewHolder {
return InternalTwoWaySyncViewHolder(
InternalTwoWaySyncViewHolderBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun getItemCount(): Int {
return folders.size
}
override fun onBindViewHolder(holder: InternalTwoWaySyncViewHolder, position: Int) {
holder.bind(folders[position], context)
}
}

View file

@ -0,0 +1,44 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.owncloud.android.ui.adapter
import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.owncloud.android.R
import com.owncloud.android.databinding.InternalTwoWaySyncViewHolderBinding
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.utils.DisplayUtils
class InternalTwoWaySyncViewHolder(val binding: InternalTwoWaySyncViewHolderBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(folder: OCFile, context: Context) {
binding.run {
size.text = DisplayUtils.bytesToHumanReadable(folder.fileLength)
name.text = folder.decryptedFileName
if (folder.internalFolderSyncResult.isEmpty()) {
syncResult.visibility = View.GONE
syncResultDivider.visibility = View.GONE
} else {
syncResult.visibility = View.VISIBLE
syncResultDivider.visibility = View.VISIBLE
syncResult.text = folder.internalFolderSyncResult
}
if (folder.internalFolderSyncTimestamp == 0L) {
syncTimestamp.text = context.getString(R.string.internal_two_way_sync_not_yet)
} else {
syncTimestamp.text = DisplayUtils.getRelativeTimestamp(
context,
folder.internalFolderSyncTimestamp
)
}
}
}
}

View file

@ -261,6 +261,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
binding.favorite.setOnClickListener(this);
binding.overflowMenu.setOnClickListener(this);
binding.lastModificationTimestamp.setOnClickListener(this);
binding.folderSyncButton.setOnClickListener(this);
updateFileDetails(false, false);
}
@ -471,8 +472,14 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
boolean showDetailedTimestamp = !preferences.isShowDetailedTimestampEnabled();
preferences.setShowDetailedTimestampEnabled(showDetailedTimestamp);
setFileModificationTimestamp(getFile(), showDetailedTimestamp);
Log_OC.e(TAG, "Incorrect view clicked!");
} else if (id == R.id.folder_sync_button) {
if (binding.folderSyncButton.isChecked()) {
getFile().setInternalFolderSyncTimestamp(0L);
} else {
getFile().setInternalFolderSyncTimestamp(-1L);
}
storageManager.saveFile(getFile());
} else {
Log_OC.e(TAG, "Incorrect view clicked!");
}
@ -556,6 +563,17 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
if (fabMain != null) {
fabMain.hide();
}
binding.syncBlock.setVisibility(file.isFolder() ? View.VISIBLE : View.GONE);
if (file.isInternalFolderSync()) {
binding.folderSyncButton.setChecked(file.isInternalFolderSync());
} else {
if (storageManager.isPartOfInternalTwoWaySync(file)) {
binding.folderSyncButton.setChecked(true);
binding.folderSyncButton.setEnabled(false);
}
}
}
setupViewPager();

View file

@ -170,6 +170,39 @@
</LinearLayout>
<LinearLayout
android:id="@+id/syncBlock"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/list_divider_background" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="@dimen/standard_padding"
android:paddingTop="@dimen/standard_half_padding"
android:paddingEnd="@dimen/zero"
android:paddingBottom="@dimen/standard_half_padding">
<CheckBox
android:id="@+id/folder_sync_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sync" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2024 Tobias Kaminsky <tobias@kaminsky.me>
~ SPDX-License-Identifier: AGPL-3.0-or-later
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2024 Andy Scherzinger <info@andy-scherzinger.de>
~ SPDX-FileCopyrightText: 2024 Tobias Kaminsky <tobias@kaminsky.me>
~ SPDX-License-Identifier: AGPL-3.0-or-later
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/min_list_item_size"
android:orientation="horizontal"
android:paddingStart="@dimen/standard_half_padding"
android:paddingEnd="@dimen/standard_half_padding">
<ImageView
android:layout_width="@dimen/file_icon_size"
android:layout_height="@dimen/file_icon_size"
android:layout_gravity="center_vertical"
android:contentDescription="@null"
android:src="@drawable/folder" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/min_list_item_size"
android:layout_marginStart="@dimen/standard_half_margin"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="@color/text_color"
android:textSize="@dimen/two_line_primary_text_size"
tools:text="Folder abc" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textColor="@color/list_item_lastmod_and_filesize_text"
android:textSize="@dimen/two_line_secondary_text_size"
tools:text="241 Mb" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:paddingStart="@dimen/zero"
android:paddingEnd="@dimen/standard_quarter_padding"
android:text="@string/info_separator"
android:textColor="@color/list_item_lastmod_and_filesize_text"
android:textSize="@dimen/two_line_secondary_text_size" />
<TextView
android:id="@+id/sync_timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textColor="@color/list_item_lastmod_and_filesize_text"
android:textSize="@dimen/two_line_secondary_text_size"
tools:text="5 min ago" />
<TextView
android:id="@+id/sync_result_divider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:paddingStart="@dimen/zero"
android:paddingEnd="@dimen/standard_quarter_padding"
android:text="@string/info_separator"
android:textColor="@color/list_item_lastmod_and_filesize_text"
android:textSize="@dimen/two_line_secondary_text_size" />
<TextView
android:id="@+id/sync_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textColor="@color/list_item_lastmod_and_filesize_text"
android:textSize="@dimen/two_line_secondary_text_size"
tools:text="Success" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View file

@ -1218,13 +1218,15 @@
<string name="sub_folder_rule_day">Year/Month/Day</string>
<string name="secure_share_not_set_up">Secure sharing is not set up for this user</string>
<string name="share_not_allowed_when_file_drop">Resharing is not allowed during secure file drop</string>
<string name="prefs_category_sync">Sync</string>
<string name="internal_two_way_sync">Internal two way sync</string>
<string name="prefs_two_way_sync_summary">Manage internal folders for two way sync</string>
<string name="internal_two_way_sync_not_yet">Not yet, soon to be synced</string>
<string name="gplay_restriction">Google restricted downloading APK/AAB files!</string>
<string name="file_list_empty_local_search">No file or folder matching your search</string>
<string name="unified_search_fragment_calendar_event_not_found">Event not found, you can always sync to update. Redirecting to web…</string>
<string name="unified_search_fragment_contact_not_found">Contact not found, you can always sync to update. Redirecting to web…</string>
<string name="unified_search_fragment_permission_needed">Permissions are required to open search result otherwise it will redirected to web…</string>
<string name="file_name_validator_current_path_is_invalid">Current folder name is invalid, please rename the folder. Redirecting to root</string>
<string name="file_name_validator_rename_before_move_or_copy">%s. Please rename the file before moving or copying</string>
<string name="file_name_validator_upload_content_error">Some contents cannot able to uploaded due to contains reserved names or invalid character</string>
@ -1233,4 +1235,5 @@
<string name="file_name_validator_error_reserved_names">%s is a forbidden name</string>
<string name="file_name_validator_error_forbidden_file_extensions">.%s is a forbidden file extension</string>
<string name="file_name_validator_error_ends_with_space_period">Name ends with a space or a period</string>
<string name="sync">Sync</string>
</resources>

View file

@ -53,13 +53,23 @@
android:key="show_media_scan_notifications"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/prefs_category_sync"
android:key="sync">
<Preference
android:title="@string/drawer_synced_folders"
android:key="syncedFolders"
android:summary="@string/prefs_sycned_folders_summary" />
<Preference
android:title="@string/internal_two_way_sync"
android:key="internal_two_way_sync"
android:summary="@string/prefs_two_way_sync_summary" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/prefs_category_more"
android:key="more">
<Preference
android:title="@string/drawer_synced_folders"
android:key="syncedFolders"
android:summary="@string/prefs_sycned_folders_summary" />
<Preference
android:title="@string/prefs_calendar_contacts"
android:key="calendar_contacts"