Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0eef5fb
add createContentValueForRemoteFile
alperozturk96 Jun 11, 2025
67f04db
add createContentValueForRemoteFile
alperozturk96 Jun 11, 2025
56d90ec
refreshSharesForFolder in share details
alperozturk96 Jun 11, 2025
42b34d4
fix wrong usages
alperozturk96 Jun 11, 2025
46911e0
fix wrong usages
alperozturk96 Jun 11, 2025
49f6b90
use lib
alperozturk96 Jun 12, 2025
d8ede2d
remove unused func
alperozturk96 Jun 12, 2025
9485503
fix kt spotless
alperozturk96 Jun 12, 2025
ed23af6
implement feedbacks
alperozturk96 Jun 12, 2025
d5263d6
indicate parameters
alperozturk96 Jun 12, 2025
79e047a
fix FileDownloadLimit crash
alperozturk96 Jun 13, 2025
21e194e
setDownloadLimitToContentValues
alperozturk96 Jun 13, 2025
8b30848
support avatar generation for external shares
alperozturk96 Jun 13, 2025
06e34c7
fix operation creation from remotefile object
alperozturk96 Jun 13, 2025
0424384
fix operation creation from remotefile object
alperozturk96 Jun 13, 2025
ea389ad
fix ss test, pass correct argument
alperozturk96 Jun 13, 2025
d3ed142
check token NPE
alperozturk96 Jun 13, 2025
c0c6b67
listen completions of the remote operation
alperozturk96 Jun 13, 2025
896662b
add shimmering
alperozturk96 Jun 13, 2025
c1729bf
add share_list_item_shimmer.xml
alperozturk96 Jun 13, 2025
083545a
add blink animation
alperozturk96 Jun 13, 2025
1d4e855
kt spotless fix
alperozturk96 Jun 13, 2025
ca8a5ee
extract layouts
alperozturk96 Jun 13, 2025
81b5a68
fix git conflict
alperozturk96 Jul 1, 2025
e1a3d57
fix git conflict
alperozturk96 Jul 18, 2025
6862a2c
fix kt spotless
alperozturk96 Jul 18, 2025
f66d6dc
use correct android lib version
alperozturk96 Aug 1, 2025
9877d0b
Rename .java to .kt
alperozturk96 Aug 1, 2025
4edd708
fetch updated data
alperozturk96 Aug 1, 2025
0ebead7
since startSyncFolderOperation called onResume and startSyncFolderOpe…
alperozturk96 Aug 1, 2025
52db915
fix build
alperozturk96 Aug 4, 2025
a3d5a31
fix kt spotless
alperozturk96 Aug 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions app/src/main/java/com/nextcloud/model/ShareeEntry.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <[email protected]>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.model

import android.content.ContentValues
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
import com.owncloud.android.lib.resources.files.model.RemoteFile
import com.owncloud.android.lib.resources.shares.ShareType

data class ShareeEntry(
val filePath: String?,
val accountOwner: String,
val fileOwnerId: String?,
val shareWithDisplayName: String?,
val shareWithUserId: String?,
val shareType: Int
) {
companion object {
/**
* Extracts a list of share-related ContentValues from a given RemoteFile.
*
* Each RemoteFile can be shared with multiple users (sharees), and this function converts each
* sharee into a ContentValues object, representing a row for insertion into a database.
*
* @param remoteFile The RemoteFile object containing sharee information.
* @param accountName The name of the user account that owns this RemoteFile.
* @return A list of ContentValues representing each share entry, or null if no sharees are found.
*/
fun getContentValues(remoteFile: RemoteFile, accountName: String): List<ContentValues>? {
if (remoteFile.sharees.isNullOrEmpty()) {
return null
}

val result = arrayListOf<ContentValues>()

for (share in remoteFile.sharees) {
val shareType: ShareType? = share?.shareType
if (shareType == null) {
continue
}

val contentValue = ShareeEntry(
remoteFile.remotePath,
accountName,
remoteFile.ownerId,
share.displayName,
share.userId,
shareType.value
).toContentValues()

result.add(contentValue)
}

return result
}
}

private fun toContentValues(): ContentValues = ContentValues().apply {
put(ProviderTableMeta.OCSHARES_PATH, filePath)
put(ProviderTableMeta.OCSHARES_ACCOUNT_OWNER, accountOwner)
put(ProviderTableMeta.OCSHARES_USER_ID, fileOwnerId)
put(ProviderTableMeta.OCSHARES_SHARE_WITH_DISPLAY_NAME, shareWithDisplayName)
put(ProviderTableMeta.OCSHARES_SHARE_WITH, shareWithUserId)
put(ProviderTableMeta.OCSHARES_SHARE_TYPE, shareType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import com.nextcloud.model.OCFileFilterType;
import com.nextcloud.model.OfflineOperationRawType;
import com.nextcloud.model.OfflineOperationType;
import com.nextcloud.model.ShareeEntry;
import com.nextcloud.utils.date.DateFormatPattern;
import com.nextcloud.utils.extensions.DateExtensionsKt;
import com.owncloud.android.MainApp;
Expand Down Expand Up @@ -111,6 +112,7 @@ public class FileDataStorageManager {
public final FileDao fileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).fileDao();
private final Gson gson = new Gson();
public final OfflineOperationsRepositoryType offlineOperationsRepository;
private final static int DEFAULT_CURSOR_INT_VALUE = -1;

public FileDataStorageManager(User user, ContentResolver contentResolver) {
this.contentProviderClient = null;
Expand Down Expand Up @@ -1563,13 +1565,7 @@ private ContentValues createContentValueForShare(OCShare share) {
contentValues.put(ProviderTableMeta.OCSHARES_SHARE_LABEL, share.getLabel());

FileDownloadLimit downloadLimit = share.getFileDownloadLimit();
if (downloadLimit != null) {
contentValues.put(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_LIMIT, downloadLimit.getLimit());
contentValues.put(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT, downloadLimit.getCount());
} else {
contentValues.putNull(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_LIMIT);
contentValues.putNull(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT);
}
setDownloadLimitToContentValues(contentValues, downloadLimit);

contentValues.put(ProviderTableMeta.OCSHARES_ATTRIBUTES, share.getAttributes());

Expand Down Expand Up @@ -1598,33 +1594,60 @@ private OCShare createShareInstance(Cursor cursor) {
share.setShareLink(getString(cursor, ProviderTableMeta.OCSHARES_SHARE_LINK));
share.setLabel(getString(cursor, ProviderTableMeta.OCSHARES_SHARE_LABEL));

FileDownloadLimit downloadLimit = new FileDownloadLimit(token,
getInt(cursor, ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_LIMIT),
getInt(cursor, ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT));
share.setFileDownloadLimit(downloadLimit);
FileDownloadLimit fileDownloadLimit = getDownloadLimitFromCursor(cursor, token);
if (fileDownloadLimit != null) {
share.setFileDownloadLimit(fileDownloadLimit);
}

share.setAttributes(getString(cursor, ProviderTableMeta.OCSHARES_ATTRIBUTES));

return share;
}

private void resetShareFlagsInAllFiles() {
ContentValues cv = new ContentValues();
cv.put(ProviderTableMeta.FILE_SHARED_VIA_LINK, Boolean.FALSE);
cv.put(ProviderTableMeta.FILE_SHARED_WITH_SHAREE, Boolean.FALSE);
String where = ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?";
String[] whereArgs = new String[]{user.getAccountName()};
private void setDownloadLimitToContentValues(ContentValues contentValues, FileDownloadLimit downloadLimit) {
if (downloadLimit != null) {
contentValues.put(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_LIMIT, downloadLimit.getLimit());
contentValues.put(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT, downloadLimit.getCount());
return;
}

if (getContentResolver() != null) {
getContentResolver().update(ProviderTableMeta.CONTENT_URI, cv, where, whereArgs);
contentValues.putNull(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_LIMIT);
contentValues.putNull(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT);
}

} else {
try {
getContentProviderClient().update(ProviderTableMeta.CONTENT_URI, cv, where, whereArgs);
} catch (RemoteException e) {
Log_OC.e(TAG, "Exception in resetShareFlagsInAllFiles" + e.getMessage(), e);
}
@Nullable
private FileDownloadLimit getDownloadLimitFromCursor(Cursor cursor, String token) {
if (token == null || cursor == null) {
return null;
}

int limit = getIntOrDefault(cursor, ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_LIMIT);
int count = getIntOrDefault(cursor, ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT);
if (limit != DEFAULT_CURSOR_INT_VALUE && count != DEFAULT_CURSOR_INT_VALUE) {
return new FileDownloadLimit(token, limit, count);
}

return null;
}

/**
* Retrieves an integer value from the specified column in the cursor.
* <p>
* If the column does not exist (i.e., {@code cursor.getColumnIndex(columnName)} returns -1),
* this method returns {@code -1} as a default value.
* </p>
*
* @param cursor The Cursor from which to retrieve the value.
* @param columnName The name of the column to retrieve the integer from.
* @return The integer value from the column, or {@code -1} if the column is not found.
*/
private int getIntOrDefault(Cursor cursor, String columnName) {
int index = cursor.getColumnIndex(columnName);
if (index == DEFAULT_CURSOR_INT_VALUE) {
return DEFAULT_CURSOR_INT_VALUE;
}

return cursor.getInt(index);
}

private void resetShareFlagsInFolder(OCFile folder) {
Expand Down Expand Up @@ -1743,6 +1766,67 @@ public void removeShare(OCShare share) {
}
}

public void saveSharesFromRemoteFile(List<RemoteFile> shares) {
if (shares == null || shares.isEmpty()) {
return;
}

// Prepare reset operations
Set<String> uniquePaths = new HashSet<>();
for (RemoteFile share : shares) {
uniquePaths.add(share.getRemotePath());
}

ArrayList<ContentProviderOperation> resetOperations = new ArrayList<>();
for (String path : uniquePaths) {
resetShareFlagInAFile(path);
var removeOps = prepareRemoveSharesInFile(path, new ArrayList<>());
if (!removeOps.isEmpty()) {
resetOperations.addAll(removeOps);
}
}
if (!resetOperations.isEmpty()) {
applyBatch(resetOperations);
}

// Prepare insert operations
ArrayList<ContentProviderOperation> insertOperations = prepareInsertSharesFromRemoteFile(shares);
if (!insertOperations.isEmpty()) {
applyBatch(insertOperations);
}
}

/**
* Prepares a list of ContentProviderOperation insert operations based on share information
* found in the given iterable of RemoteFile objects.
* <p>
* Each RemoteFile may have multiple share entries (sharees), and for each one,
* a corresponding ContentProviderOperation is created for insertion into the shares table.
*
* @param remoteFiles An iterable list of RemoteFile objects containing sharee data.
* @return A list of ContentProviderOperation objects for batch insertion into the content provider.
*/
private ArrayList<ContentProviderOperation> prepareInsertSharesFromRemoteFile(Iterable<RemoteFile> remoteFiles) {
final ArrayList<ContentValues> contentValueList = new ArrayList<>();
for (RemoteFile remoteFile : remoteFiles) {
final var contentValues = ShareeEntry.Companion.getContentValues(remoteFile, user.getAccountName());
if (contentValues == null) {
continue;
}
contentValueList.addAll(contentValues);
}

ArrayList<ContentProviderOperation> operations = new ArrayList<>();
for (ContentValues contentValues : contentValueList) {
operations.add(ContentProviderOperation
.newInsert(ProviderTableMeta.CONTENT_URI_SHARE)
.withValues(contentValues)
.build());
}

return operations;
}

public void saveSharesDB(List<OCShare> shares) {
ArrayList<ContentProviderOperation> operations = new ArrayList<>();

Expand All @@ -1759,20 +1843,26 @@ public void saveSharesDB(List<OCShare> shares) {
// Add operations to insert shares
operations = prepareInsertShares(shares, operations);

if (operations.isEmpty()) {
return;
}

// apply operations in batch
if (operations.size() > 0) {
Log_OC.d(TAG, String.format(Locale.ENGLISH, SENDING_TO_FILECONTENTPROVIDER_MSG, operations.size()));
try {
if (getContentResolver() != null) {
getContentResolver().applyBatch(MainApp.getAuthority(), operations);
Log_OC.d(TAG, String.format(Locale.ENGLISH, SENDING_TO_FILECONTENTPROVIDER_MSG, operations.size()));
applyBatch(operations);
}

} else {
getContentProviderClient().applyBatch(operations);
}
private void applyBatch(ArrayList<ContentProviderOperation> operations) {
try {
if (getContentResolver() != null) {
getContentResolver().applyBatch(MainApp.getAuthority(), operations);

} catch (OperationApplicationException | RemoteException e) {
Log_OC.e(TAG, EXCEPTION_MSG + e.getMessage(), e);
} else {
getContentProviderClient().applyBatch(operations);
}

} catch (OperationApplicationException | RemoteException e) {
Log_OC.e(TAG, EXCEPTION_MSG + e.getMessage(), e);
}
}

Expand Down Expand Up @@ -1830,8 +1920,7 @@ public void saveSharesInFolder(ArrayList<OCShare> shares, OCFile folder) {
* @param operations List of operations
* @return
*/
private ArrayList<ContentProviderOperation> prepareInsertShares(
Iterable<OCShare> shares, ArrayList<ContentProviderOperation> operations) {
private ArrayList<ContentProviderOperation> prepareInsertShares(Iterable<OCShare> shares, ArrayList<ContentProviderOperation> operations) {

ContentValues contentValues;
// prepare operations to insert or update files to save in the given folder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ protected RemoteOperationResult run(OwnCloudClient client) {
mConflictsFound = 0;
mForgottenLocalFiles.clear();

if (mLocalFolder == null) {
Log_OC.e(TAG, "Local folder is null, cannot run refresh folder operation");
return new RemoteOperationResult<>(ResultCode.FILE_NOT_FOUND);
}

if (OCFile.ROOT_PATH.equals(mLocalFolder.getRemotePath()) && !mSyncFullAccount && !mOnlyFileMetadata) {
updateOCVersion(client);
updateUserProfile();
Expand All @@ -253,19 +258,32 @@ protected RemoteOperationResult run(OwnCloudClient client) {
mLocalFolder.setEtag("");
}

mLocalFolder.setLastSyncDateForData(System.currentTimeMillis());
fileDataStorageManager.saveFile(mLocalFolder);
if (mLocalFolder != null) {
mLocalFolder.setLastSyncDateForData(System.currentTimeMillis());
fileDataStorageManager.saveFile(mLocalFolder);
} else {
Log_OC.e(TAG, "Local folder is null, cannot set last sync date nor save file");
result = new RemoteOperationResult<>(ResultCode.FILE_NOT_FOUND);
}
}

if (!mSyncFullAccount && mRemoteFolderChanged) {
if (!mSyncFullAccount && mRemoteFolderChanged && mLocalFolder != null) {
sendLocalBroadcast(EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result);
}

if (result.isSuccess() && !mSyncFullAccount && !mOnlyFileMetadata) {
refreshSharesForFolder(client); // share result is ignored
if (result.isSuccess() && result.getData() != null && !mSyncFullAccount && !mOnlyFileMetadata) {
final var remoteObject = result.getData();
final ArrayList<RemoteFile> remoteFiles = new ArrayList<>();
for (Object object: remoteObject) {
if (object instanceof RemoteFile remoteFile) {
remoteFiles.add(remoteFile);
}
}

fileDataStorageManager.saveSharesFromRemoteFile(remoteFiles);
}

if (!mSyncFullAccount) {
if (!mSyncFullAccount && mLocalFolder != null) {
sendLocalBroadcast(EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result);
}

Expand Down Expand Up @@ -781,16 +799,6 @@ private void startContentSynchronizations(List<SynchronizeFileOperation> filesTo
}
}

/**
* Syncs the Share resources for the files contained in the folder refreshed (children, not deeper descendants).
*
* @param client Handler of a session with an OC server.
*/
private void refreshSharesForFolder(OwnCloudClient client) {
GetSharesForFileOperation operation = new GetSharesForFileOperation(mLocalFolder.getRemotePath(), true, true, fileDataStorageManager);
operation.execute(client);
}

/**
* Sends a message to any application component interested in the progress of the synchronization.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,13 @@ private Uri insert(SupportSQLiteDatabase db, Uri uri, ContentValues values) {

private void updateFilesTableAccordingToShareInsertion(SupportSQLiteDatabase db, ContentValues newShare) {
ContentValues fileValues = new ContentValues();
ShareType newShareType = ShareType.fromValue(newShare.getAsInteger(ProviderTableMeta.OCSHARES_SHARE_TYPE));
Integer shareTypeValue = newShare.getAsInteger(ProviderTableMeta.OCSHARES_SHARE_TYPE);
if (shareTypeValue == null) {
Log_OC.w(TAG, "Share type is null. Skipping file update.");
return;
}

ShareType newShareType = ShareType.fromValue(shareTypeValue);

switch (newShareType) {
case PUBLIC_LINK:
Expand Down
Loading
Loading