Skip to content

Commit 9d878e5

Browse files
Merge pull request #17583 from nextcloud/backport/17082/stable-35.0.x
[stable-35.0.x] performance: move local file
2 parents 76e3719 + 261cf75 commit 9d878e5

11 files changed

Lines changed: 926 additions & 123 deletions

File tree

app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,7 @@ interface FileDao {
202202

203203
@Query("UPDATE filelist SET is_read_only = :readOnly WHERE file_owner = :fileOwner AND path = :path")
204204
fun setReadOnly(fileOwner: String, path: String, readOnly: Int): Int
205+
206+
@Update
207+
fun updateAll(entities: List<FileEntity>)
205208
}

app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@
77

88
package com.nextcloud.utils.extensions
99

10+
import com.nextcloud.client.database.dao.FileDao
1011
import com.nextcloud.client.database.entity.model.ShareeKey
1112
import com.nextcloud.client.database.entity.toOCCapability
1213
import com.owncloud.android.datamodel.FileDataStorageManager
1314
import com.owncloud.android.datamodel.OCFile
15+
import com.owncloud.android.lib.common.utils.Log_OC
1416
import com.owncloud.android.lib.resources.files.model.RemoteFile
1517
import com.owncloud.android.lib.resources.shares.OCShare
1618
import com.owncloud.android.lib.resources.status.OCCapability
19+
import com.owncloud.android.utils.FileStorageUtils
20+
import com.owncloud.android.utils.MimeTypeUtil
1721
import kotlinx.coroutines.Dispatchers
1822
import kotlinx.coroutines.withContext
23+
import java.io.File
1924

2025
private const val SHARE_PATH_QUERY_CHUNK_SIZE = 400
2126

@@ -116,3 +121,131 @@ fun FileDataStorageManager.getNonEncryptedSubfolders(id: Long, accountName: Stri
116121

117122
suspend fun FileDataStorageManager.getCapabilitiesByAccountName(accountName: String): OCCapability =
118123
capabilityDao.getByAccountName(accountName).toOCCapability()
124+
125+
@Suppress("ReturnCount")
126+
fun FileDataStorageManager.moveFiles(ocFile: OCFile?, targetPath: String, targetParentPath: String) {
127+
Log_OC.d(
128+
FileDataStorageManager.TAG,
129+
(
130+
"moveLocalFile ==> ocFile: " +
131+
(ocFile?.remotePath) +
132+
" targetPath: " +
133+
targetPath +
134+
" targetParentPath: " +
135+
targetParentPath
136+
)
137+
)
138+
139+
if (ocFile == null) {
140+
Log_OC.e(FileDataStorageManager.TAG, "moveLocalFile: file is null, skipping")
141+
return
142+
}
143+
144+
if (!ocFile.fileExists()) {
145+
Log_OC.e(FileDataStorageManager.TAG, "moveLocalFile: file does not exist, skipping")
146+
return
147+
}
148+
149+
if (OCFile.ROOT_PATH == ocFile.fileName) {
150+
Log_OC.w(FileDataStorageManager.TAG, "moveLocalFile: cannot move root path")
151+
return
152+
}
153+
154+
if (ocFile.remotePath == targetPath) {
155+
Log_OC.w(FileDataStorageManager.TAG, "moveLocalFile: source and target paths are identical, skipping")
156+
return
157+
}
158+
159+
val targetParent = getFileByPath(targetParentPath)
160+
if (targetParent == null) {
161+
Log_OC.e(FileDataStorageManager.TAG, "moveLocalFile: target parent folder not found: $targetParentPath")
162+
return
163+
}
164+
165+
if (!targetParent.isFolder) {
166+
Log_OC.e(FileDataStorageManager.TAG, "moveLocalFile: target parent is not a folder: $targetParentPath")
167+
return
168+
}
169+
170+
val oldPath: String = ocFile.remotePath
171+
val accountName = user.accountName
172+
val defaultSavePath = FileStorageUtils.getSavePath(accountName)
173+
174+
val originalMediaPaths =
175+
fileDao.moveFilesInDb(oldPath, targetPath, defaultSavePath, targetParent.fileId, accountName)
176+
177+
if (!moveLocalFiles(accountName, ocFile, defaultSavePath, targetPath)) return
178+
179+
for (originalMediaPath in originalMediaPaths) {
180+
deleteFileInMediaScan(originalMediaPath)
181+
val newMediaPath = defaultSavePath + targetPath + originalMediaPath.substring(
182+
(defaultSavePath + oldPath).length
183+
)
184+
FileDataStorageManager.triggerMediaScan(newMediaPath)
185+
}
186+
}
187+
188+
@Suppress("ReturnCount")
189+
private fun moveLocalFiles(accountName: String, ocFile: OCFile, defaultSavePath: String, targetPath: String): Boolean {
190+
val localFile = File(FileStorageUtils.getDefaultSavePathFor(accountName, ocFile))
191+
if (!localFile.exists()) {
192+
Log_OC.d(FileDataStorageManager.TAG, "moveLocalFile: no local file to move at " + localFile.absolutePath)
193+
return false
194+
}
195+
196+
val targetFile = File(defaultSavePath + targetPath)
197+
val targetFolder = targetFile.getParentFile()
198+
if (targetFolder != null && !targetFolder.exists() && !targetFolder.mkdirs()) {
199+
Log_OC.e(
200+
FileDataStorageManager.TAG,
201+
"moveLocalFile: failed to create parent folder " + targetFolder.absolutePath
202+
)
203+
}
204+
205+
if (!localFile.renameTo(targetFile)) {
206+
Log_OC.e(
207+
FileDataStorageManager.TAG,
208+
(
209+
"moveLocalFile: failed to rename " + localFile.absolutePath +
210+
" to " + targetFile.absolutePath
211+
)
212+
)
213+
return false
214+
}
215+
216+
return true
217+
}
218+
219+
private fun FileDao.moveFilesInDb(
220+
oldPath: String,
221+
targetPath: String,
222+
defaultSavePath: String,
223+
targetParentId: Long,
224+
accountName: String
225+
): List<String> {
226+
val entities = getFolderWithDescendants("$oldPath%", accountName)
227+
val oldStoragePrefix = defaultSavePath + oldPath
228+
val newStoragePrefix = defaultSavePath + targetPath
229+
230+
val originalMediaPaths = entities
231+
.filter { MimeTypeUtil.isMedia(it.contentType) && it.storagePath?.startsWith(oldStoragePrefix) == true }
232+
.mapNotNull { it.storagePath }
233+
234+
val updated = entities.map { entity ->
235+
val currentPath = entity.path.orEmpty()
236+
val newPath = targetPath + currentPath.substring(oldPath.length)
237+
entity.copy(
238+
path = newPath,
239+
pathDecrypted = if (entity.isEncrypted == 1) entity.pathDecrypted else newPath,
240+
storagePath = if (entity.storagePath?.startsWith(oldStoragePrefix) == true) {
241+
newStoragePrefix + entity.storagePath.substring(oldStoragePrefix.length)
242+
} else {
243+
entity.storagePath
244+
},
245+
parent = if (currentPath == oldPath) targetParentId else entity.parent
246+
)
247+
}
248+
249+
updateAll(updated)
250+
return originalMediaPaths
251+
}

app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

Lines changed: 2 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696

9797
@SuppressFBWarnings("CE")
9898
public class FileDataStorageManager {
99-
private static final String TAG = FileDataStorageManager.class.getSimpleName();
99+
public static final String TAG = FileDataStorageManager.class.getSimpleName();
100100

101101
private static final String AND = " = ? AND ";
102102
private static final String FAILED_TO_INSERT_MSG = "Fail to insert insert file to database ";
@@ -1127,113 +1127,9 @@ private boolean removeLocalFolder(File localFolder) {
11271127

11281128
/**
11291129
* Updates database and file system for a file or folder that was moved to a different location.
1130-
* <p>
1131-
* TODO explore better (faster) implementations TODO throw exceptions up !
11321130
*/
11331131
public void moveLocalFile(OCFile ocFile, String targetPath, String targetParentPath) {
1134-
if (ocFile.fileExists() && !OCFile.ROOT_PATH.equals(ocFile.getFileName())) {
1135-
1136-
OCFile targetParent = getFileByPath(targetParentPath);
1137-
if (targetParent == null) {
1138-
throw new IllegalStateException("Parent folder of the target path does not exist!!");
1139-
}
1140-
1141-
String oldPath = ocFile.getRemotePath();
1142-
1143-
/// 1. get all the descendants of the moved element in a single QUERY
1144-
List<FileEntity> fileEntities =
1145-
fileDao.getFolderWithDescendants(oldPath + "%", user.getAccountName());
1146-
1147-
/// 2. prepare a batch of update operations to change all the descendants
1148-
ArrayList<ContentProviderOperation> operations = new ArrayList<>(fileEntities.size());
1149-
String defaultSavePath = FileStorageUtils.getSavePath(user.getAccountName());
1150-
List<String> originalPathsToTriggerMediaScan = new ArrayList<>();
1151-
List<String> newPathsToTriggerMediaScan = new ArrayList<>();
1152-
1153-
int lengthOfOldPath = oldPath.length();
1154-
int lengthOfOldStoragePath = defaultSavePath.length() + lengthOfOldPath;
1155-
for (FileEntity fileEntity : fileEntities) {
1156-
ContentValues contentValues = new ContentValues(); // keep construction in the loop
1157-
OCFile childFile = createFileInstance(fileEntity);
1158-
contentValues.put(
1159-
ProviderTableMeta.FILE_PATH,
1160-
targetPath + childFile.getRemotePath().substring(lengthOfOldPath)
1161-
);
1162-
1163-
if (!childFile.isEncrypted()) {
1164-
contentValues.put(
1165-
ProviderTableMeta.FILE_PATH_DECRYPTED,
1166-
targetPath + childFile.getRemotePath().substring(lengthOfOldPath)
1167-
);
1168-
}
1169-
1170-
if (childFile.getStoragePath() != null && childFile.getStoragePath().startsWith(defaultSavePath)) {
1171-
// update link to downloaded content - but local move is not done here!
1172-
String targetLocalPath = defaultSavePath + targetPath +
1173-
childFile.getStoragePath().substring(lengthOfOldStoragePath);
1174-
1175-
contentValues.put(ProviderTableMeta.FILE_STORAGE_PATH, targetLocalPath);
1176-
1177-
if (MimeTypeUtil.isMedia(childFile.getMimeType())) {
1178-
originalPathsToTriggerMediaScan.add(childFile.getStoragePath());
1179-
newPathsToTriggerMediaScan.add(targetLocalPath);
1180-
}
1181-
1182-
}
1183-
1184-
if (childFile.getRemotePath().equals(ocFile.getRemotePath())) {
1185-
contentValues.put(ProviderTableMeta.FILE_PARENT, targetParent.getFileId());
1186-
}
1187-
1188-
operations.add(
1189-
ContentProviderOperation.newUpdate(ProviderTableMeta.CONTENT_URI)
1190-
.withValues(contentValues)
1191-
.withSelection(ProviderTableMeta._ID + " = ?", new String[]{String.valueOf(childFile.getFileId())})
1192-
.build());
1193-
1194-
}
1195-
1196-
/// 3. apply updates in batch
1197-
try {
1198-
if (getContentResolver() != null) {
1199-
getContentResolver().applyBatch(MainApp.getAuthority(), operations);
1200-
} else {
1201-
getContentProviderClient().applyBatch(operations);
1202-
}
1203-
1204-
} catch (Exception e) {
1205-
Log_OC.e(TAG, "Fail to update " + ocFile.getFileId() + " and descendants in database", e);
1206-
}
1207-
1208-
/// 4. move in local file system
1209-
String originalLocalPath = FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), ocFile);
1210-
String targetLocalPath = defaultSavePath + targetPath;
1211-
File localFile = new File(originalLocalPath);
1212-
boolean renamed = false;
1213-
1214-
if (localFile.exists()) {
1215-
File targetFile = new File(targetLocalPath);
1216-
File targetFolder = targetFile.getParentFile();
1217-
if (targetFolder != null && !targetFolder.exists() && !targetFolder.mkdirs()) {
1218-
Log_OC.e(TAG, "Unable to create parent folder " + targetFolder.getAbsolutePath());
1219-
}
1220-
renamed = localFile.renameTo(targetFile);
1221-
}
1222-
1223-
if (renamed) {
1224-
Iterator<String> pathIterator = originalPathsToTriggerMediaScan.iterator();
1225-
while (pathIterator.hasNext()) {
1226-
// Notify MediaScanner about removed file
1227-
deleteFileInMediaScan(pathIterator.next());
1228-
}
1229-
1230-
pathIterator = newPathsToTriggerMediaScan.iterator();
1231-
while (pathIterator.hasNext()) {
1232-
// Notify MediaScanner about new file/folder
1233-
triggerMediaScan(pathIterator.next());
1234-
}
1235-
}
1236-
}
1132+
FileDataStorageManagerExtensionsKt.moveFiles(this, ocFile, targetPath, targetParentPath);
12371133
}
12381134

12391135
public void copyLocalFile(OCFile ocFile, String targetPath) {

app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@
8080
import java.util.Locale;
8181
import java.util.Set;
8282
import java.util.UUID;
83-
import java.util.stream.IntStream;
8483

8584
import androidx.annotation.NonNull;
8685
import androidx.annotation.Nullable;
@@ -1124,18 +1123,17 @@ public void removeFile(@NonNull OCFile file) {
11241123

11251124
@SuppressLint("NotifyDataSetChanged")
11261125
public void updateFile(@NonNull OCFile updatedFile) {
1127-
long fileId = updatedFile.getFileId();
1126+
int allIndex = helper.indexOfSameRemoteFile(mFilesAll, updatedFile);
1127+
if (allIndex != -1) {
1128+
mFilesAll.set(allIndex, updatedFile);
1129+
}
11281130

1129-
IntStream.range(0, mFilesAll.size())
1130-
.filter(i -> mFilesAll.get(i).getFileId() == fileId)
1131-
.findFirst()
1132-
.ifPresent(i -> mFilesAll.set(i, updatedFile));
1131+
int oldIndex = helper.indexOfSameRemoteFile(mFiles, updatedFile);
1132+
if (oldIndex == -1) {
1133+
return;
1134+
}
11331135

1134-
int oldIndex = IntStream.range(0, mFiles.size())
1135-
.filter(i -> mFiles.get(i).getFileId() == fileId)
1136-
.findFirst()
1137-
.orElse(-1);
1138-
if (oldIndex == -1) return;
1136+
long previousItemId = mFiles.get(oldIndex).getFileId();
11391137

11401138
mFiles.remove(oldIndex);
11411139
mFiles.add(updatedFile);
@@ -1159,10 +1157,12 @@ public void updateFile(@NonNull OCFile updatedFile) {
11591157
int oldAdapterPos = oldIndex + headerOffset;
11601158
int newAdapterPos = newIndex + headerOffset;
11611159

1162-
if (oldAdapterPos != newAdapterPos) {
1163-
notifyItemMoved(oldAdapterPos, newAdapterPos);
1160+
if (oldAdapterPos == newAdapterPos && previousItemId == updatedFile.getFileId()) {
1161+
notifyItemChanged(newAdapterPos);
1162+
} else {
1163+
notifyItemRemoved(oldAdapterPos);
1164+
notifyItemInserted(newAdapterPos);
11641165
}
1165-
notifyItemChanged(newAdapterPos);
11661166

11671167
if (shouldShowRecommendedFiles() && recommendedFilesAdapter != null && updatedFile.isRecommendedFile()) {
11681168
int pos = recommendedFilesAdapter.getItemPosition(updatedFile);

app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,19 @@ class OCFileListAdapterHelper {
196196
}
197197
}
198198

199+
fun indexOfSameRemoteFile(files: List<OCFile>, target: OCFile): Int =
200+
files.indexOfFirst { isSameRemoteFile(it, target) }
201+
202+
@Suppress("ReturnCount")
203+
fun isSameRemoteFile(file: OCFile, target: OCFile): Boolean {
204+
if (file.fileId == target.fileId) {
205+
return true
206+
}
207+
208+
val remoteId = file.remoteId ?: return false
209+
return remoteId == target.remoteId
210+
}
211+
199212
fun cleanup() {
200213
job?.cancel()
201214
job = null

app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -954,16 +954,14 @@ public void toggleFileLock(OCFile file, boolean shouldBeLocked) {
954954
}
955955
}
956956

957-
public void renameFile(OCFile file, String newFilename) {
957+
public void renameFile(ServerFileInterface file, String newFilename) {
958958
Intent service = new Intent(fileActivity, OperationsService.class);
959959

960960
service.setAction(OperationsService.ACTION_RENAME);
961961
service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount());
962962
service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
963963
service.putExtra(OperationsService.EXTRA_NEWNAME, newFilename);
964964
mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service);
965-
966-
fileActivity.refreshList();
967965
}
968966

969967

0 commit comments

Comments
 (0)