Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
android:icon="@mipmap/ic_launcher"
android:installLocation="internalOnly"
android:label="@string/app_name"
android:largeHeap="true"
android:manageSpaceActivity="com.owncloud.android.ui.activity.ManageSpaceActivity"
android:memtagMode="async"
android:networkSecurityConfig="@xml/network_security_config"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ class AutoUploadWorker(
val dateTime = formatter.parse(exifDate, pos)
if (dateTime != null) {
lastModificationTime = dateTime.time
Log_OC.w(TAG, "calculateLastModificationTime calculatedTime is: $lastModificationTime")
Log_OC.i(TAG, "calculateLastModificationTime calculatedTime is: $lastModificationTime")
} else {
Log_OC.w(TAG, "calculateLastModificationTime dateTime is empty")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ object UploadErrorNotificationManager {
result.code == ResultCode.USER_CANCELLED ||
operation.isMissingPermissionThrown
) {
Log_OC.w(TAG, "operation is successful, cancelled or lack of storage permission")
Log_OC.i(TAG, "operation is successful, cancelled or lack of storage permission")
return false
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public class FileDataStorageManager {
private static final String FAILED_TO_INSERT_MSG = "Fail to insert insert file to database ";
private static final String SENDING_TO_FILECONTENTPROVIDER_MSG = "Sending %d operations to FileContentProvider";
private static final String EXCEPTION_MSG = "Exception in batch of operations ";
private static final int BATCH_SIZE = 500; // Maximum number of operations per batch to avoid memory issues

public static final int ROOT_PARENT_ID = 0;
private static final String JSON_NULL_STRING = "null";
Expand Down Expand Up @@ -670,13 +671,59 @@ public void saveNewFile(OCFile newFile) {
* @param filesToRemove
*/
public void saveFolder(OCFile folder, List<OCFile> updatedFiles, Collection<OCFile> filesToRemove) {
String threadName = Thread.currentThread().getName();
Log_OC.d(TAG, "Saving folder " + folder.getRemotePath() + " with " + updatedFiles.size()
+ " children and " + filesToRemove.size() + " files to remove");
+ " children and " + filesToRemove.size() + " files to remove [Thread: " + threadName + "]");

// Process files in batches to avoid memory issues with large folders
int totalFiles = updatedFiles.size();
if (totalFiles > BATCH_SIZE) {
Log_OC.d(TAG, "Large folder detected (" + totalFiles + " files). Processing in batches of " + BATCH_SIZE + " [Thread: " + threadName + "]");

// Process files in batches
for (int i = 0; i < totalFiles; i += BATCH_SIZE) {
int endIndex = Math.min(i + BATCH_SIZE, totalFiles);
List<OCFile> batchFiles = updatedFiles.subList(i, endIndex);
saveFolderBatch(folder, batchFiles, i);
}
} else {
// Small folder - process normally
saveFolderBatch(folder, updatedFiles, 0);
}

ArrayList<ContentProviderOperation> operations = new ArrayList<>(updatedFiles.size());
// Process deletions separately
if (!filesToRemove.isEmpty()) {
processFileRemovals(folder, filesToRemove);
}

// Update folder metadata (always last)
updateFolderMetadata(folder);
}

/**
* Saves a batch of files to the database without updating folder metadata.
* This is used when processing large folders in batches to avoid memory issues.
*
* @param folder The parent folder
* @param batchFiles The files to save in this batch
* @param startIndex The starting index in the original list (for logging)
*/
public void saveFolderBatchOnly(OCFile folder, List<OCFile> batchFiles, int startIndex) {
saveFolderBatch(folder, batchFiles, startIndex);
}

/**
* Saves a batch of files to the database.
*
* @param folder The parent folder
* @param batchFiles The files to save in this batch
* @param startIndex The starting index in the original list (for logging)
*/
private void saveFolderBatch(OCFile folder, List<OCFile> batchFiles, int startIndex) {
ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchFiles.size());

// prepare operations to insert or update files to save in the given folder
for (OCFile ocFile : updatedFiles) {
for (OCFile ocFile : batchFiles) {
ContentValues contentValues = createContentValuesForFile(ocFile);
contentValues.put(ProviderTableMeta.FILE_PARENT, folder.getFileId());

Expand All @@ -700,10 +747,51 @@ public void saveFolder(OCFile folder, List<OCFile> updatedFiles, Collection<OCFi
}
}

// prepare operations to remove files in the given folder
// apply operations in batch
ContentProviderResult[] results = null;
Log_OC.d(TAG, String.format(Locale.ENGLISH, SENDING_TO_FILECONTENTPROVIDER_MSG + " (batch starting at index %d)",
operations.size(), startIndex));

try {
if (getContentResolver() != null) {
results = getContentResolver().applyBatch(MainApp.getAuthority(), operations);
} else {
results = getContentProviderClient().applyBatch(operations);
}
} catch (OperationApplicationException | RemoteException e) {
Log_OC.e(TAG, EXCEPTION_MSG + e.getMessage(), e);
return;
}

// update new id in file objects for insertions
if (results != null) {
Iterator<OCFile> fileIterator = batchFiles.iterator();
for (ContentProviderResult result : results) {
OCFile ocFile = fileIterator.hasNext() ? fileIterator.next() : null;
if (result.uri != null && ocFile != null) {
try {
long newId = Long.parseLong(result.uri.getPathSegments().get(1));
ocFile.setFileId(newId);
} catch (NumberFormatException | IndexOutOfBoundsException e) {
Log_OC.e(TAG, "Failed to parse file ID from URI: " + result.uri, e);
}
}
}
}
}

/**
* Processes file removals for a folder.
*
* @param folder The parent folder
* @param filesToRemove The files to remove
*/
public void processFileRemovals(OCFile folder, Collection<OCFile> filesToRemove) {
ArrayList<ContentProviderOperation> operations = new ArrayList<>();
String where = ProviderTableMeta.FILE_ACCOUNT_OWNER + AND + ProviderTableMeta.FILE_PATH + " = ?";
String[] whereArgs = new String[2];
whereArgs[0] = user.getAccountName();

for (OCFile ocFile : filesToRemove) {
if (ocFile.getParentId() == folder.getFileId()) {
whereArgs[1] = ocFile.getRemotePath();
Expand Down Expand Up @@ -731,49 +819,43 @@ public void saveFolder(OCFile folder, List<OCFile> updatedFiles, Collection<OCFi
}
}

// update metadata of folder
ContentValues contentValues = createContentValuesForFolder(folder);
if (!operations.isEmpty()) {
Log_OC.d(TAG, String.format(Locale.ENGLISH, SENDING_TO_FILECONTENTPROVIDER_MSG + " (deletions)", operations.size()));
try {
if (getContentResolver() != null) {
getContentResolver().applyBatch(MainApp.getAuthority(), operations);
} else {
getContentProviderClient().applyBatch(operations);
}
} catch (OperationApplicationException | RemoteException e) {
Log_OC.e(TAG, EXCEPTION_MSG + e.getMessage(), e);
}
}
}

/**
* Updates the metadata of a folder.
*
* @param folder The folder to update
*/
public void updateFolderMetadata(OCFile folder) {
ContentValues contentValues = createContentValuesForFolder(folder);
ArrayList<ContentProviderOperation> operations = new ArrayList<>(1);

operations.add(ContentProviderOperation.newUpdate(ProviderTableMeta.CONTENT_URI)
.withValues(contentValues)
.withSelection(ProviderTableMeta._ID + " = ?", new String[]{String.valueOf(folder.getFileId())})
.build());

// apply operations in batch
ContentProviderResult[] results = null;
Log_OC.d(TAG, String.format(Locale.ENGLISH, SENDING_TO_FILECONTENTPROVIDER_MSG, operations.size()));

try {
if (getContentResolver() != null) {
results = getContentResolver().applyBatch(MainApp.getAuthority(), operations);

getContentResolver().applyBatch(MainApp.getAuthority(), operations);
} else {
results = getContentProviderClient().applyBatch(operations);
getContentProviderClient().applyBatch(operations);
}

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

// update new id in file objects for insertions
if (results != null) {
long newId;
Iterator<OCFile> fileIterator = updatedFiles.iterator();
OCFile ocFile;
for (ContentProviderResult result : results) {
if (fileIterator.hasNext()) {
ocFile = fileIterator.next();
} else {
ocFile = null;
}
if (result.uri != null) {
newId = Long.parseLong(result.uri.getPathSegments().get(1));
if (ocFile != null) {
ocFile.setFileId(newId);
}
}
}
}
}

/**
Expand Down
13 changes: 12 additions & 1 deletion app/src/main/java/com/owncloud/android/datamodel/OCFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,18 @@ public long getLocalId() {
if (localId > 0) {
return localId;
} else if (remoteId != null && remoteId.length() > 8) {
return Long.parseLong(remoteId.substring(0, 8).replaceAll("^0*", ""));
try {
// Only try to parse as number if it looks like a number (starts with digit)
String potentialNumber = remoteId.substring(0, 8).replaceAll("^0*", "");
if (potentialNumber.matches("\\d+")) {
return Long.parseLong(potentialNumber);
} else {
return -1;
}
} catch (NumberFormatException e) {
Log_OC.w("OCFile", "Failed to parse localId from remoteId: '" + remoteId + "', returning -1");
return -1;
}
} else {
return -1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.URLEncoder;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
Expand Down Expand Up @@ -644,9 +645,24 @@ private Bitmap doThumbnailFromOCFileInBackground() {
try {
String uri;
if (file instanceof OCFile) {
uri = mClient.getBaseUri() + "/index.php/core/preview?fileId="
+ file.getLocalId()
+ "&x=" + pxW + "&y=" + pxH + "&a=1&mode=cover&forceIcon=0";
long localId = file.getLocalId();
if (localId > 0) {
// Use fileId if available
uri = mClient.getBaseUri() + "/index.php/core/preview?fileId="
+ localId
+ "&x=" + pxW + "&y=" + pxH + "&a=1&mode=cover&forceIcon=0";
} else {
// Try different API endpoints for Nextcloud 31
String filePath = ((OCFile) file).getRemotePath();
// Try Nextcloud 31 files API thumbnail endpoint
String cleanPath = filePath.startsWith("/") ? filePath.substring(1) : filePath;
try {
cleanPath = URLEncoder.encode(cleanPath, "UTF-8");
} catch (Exception e) {
// Ignore encoding errors
}
uri = mClient.getBaseUri() + "/index.php/apps/files/api/v1/thumbnail/" + pxW + "/" + pxH + "/" + cleanPath;
}
} else {
uri = mClient.getBaseUri() + "/index.php/apps/files_trashbin/preview?fileId="
+ file.getLocalId() + "&x=" + pxW + "&y=" + pxH;
Expand Down
Loading
Loading