diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java b/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java deleted file mode 100644 index ca4c416f26a9..000000000000 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2017 JARP - * SPDX-FileCopyrightText: 2021 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.datamodel; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.ActivityNotFoundException; -import android.content.ContentResolver; -import android.content.Context; - -import com.nextcloud.client.account.CurrentAccountProvider; -import com.nextcloud.client.account.User; -import com.nextcloud.client.account.UserAccountManager; -import com.nextcloud.client.account.UserAccountManagerImpl; -import com.nextcloud.client.database.entity.UploadEntityKt; -import com.nextcloud.test.RandomStringGenerator; -import com.owncloud.android.AbstractIT; -import com.owncloud.android.MainApp; -import com.owncloud.android.db.OCUpload; -import com.owncloud.android.db.UploadResult; -import com.owncloud.android.files.services.NameCollisionPolicy; -import com.owncloud.android.lib.common.accounts.AccountUtils; -import com.owncloud.android.operations.UploadFileOperation; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.File; -import java.util.ArrayList; -import java.util.Random; -import java.util.UUID; - -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -/** - * Created by JARP on 6/7/17. - */ -@RunWith(AndroidJUnit4.class) -@SmallTest -public class UploadStorageManagerTest extends AbstractIT { - private UploadsStorageManager uploadsStorageManager; - private CurrentAccountProvider currentAccountProvider = () -> null; - private UserAccountManager userAccountManager; - private User user2; - - @Before - public void setUp() { - Context instrumentationCtx = ApplicationProvider.getApplicationContext(); - ContentResolver contentResolver = instrumentationCtx.getContentResolver(); - uploadsStorageManager = new UploadsStorageManager(currentAccountProvider, contentResolver); - userAccountManager = UserAccountManagerImpl.fromContext(targetContext); - - Account temp = new Account("test2@test.com", MainApp.getAccountType(targetContext)); - if (!userAccountManager.exists(temp)) { - AccountManager platformAccountManager = AccountManager.get(targetContext); - platformAccountManager.addAccountExplicitly(temp, "testPassword", null); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION, - Integer.toString(UserAccountManager.ACCOUNT_VERSION)); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_VERSION, "14.0.0.0"); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_BASE_URL, "test.com"); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_USER_ID, "test"); // same as userId - } - - final UserAccountManager userAccountManager = UserAccountManagerImpl.fromContext(targetContext); - user2 = userAccountManager.getUser("test2@test.com").orElseThrow(ActivityNotFoundException::new); - } - - @Test - public void testDeleteAllUploads() { - // Clean - for (User user : userAccountManager.getAllUsers()) { - uploadsStorageManager.removeUserUploads(user); - } - int accountRowsA = 3; - int accountRowsB = 4; - insertUploads(account, accountRowsA); - insertUploads(user2.toPlatformAccount(), accountRowsB); - - assertEquals("Expected 4 removed uploads files", - 4, - uploadsStorageManager.removeUserUploads(user2)); - } - - @Test - public void largeTest() { - int size = 3000; - ArrayList uploads = new ArrayList<>(); - - deleteAllUploads(); - assertEquals(0, uploadsStorageManager.getAllStoredUploads().length); - - for (int i = 0; i < size; i++) { - OCUpload upload = createUpload(account); - - uploads.add(upload); - uploadsStorageManager.uploadDao.insertOrReplace(UploadEntityKt.toUploadEntity(upload)); - } - - OCUpload[] storedUploads = uploadsStorageManager.getAllStoredUploads(); - assertEquals(size, storedUploads.length); - assertEquals(uploads.size(), storedUploads.length); - - for (int i = 0; i < size; i++) { - assertTrue("Upload " + (i+1) + "/" + size + " not found in stored uploads: " + storedUploads[i].getLocalPath(), - contains(uploads, storedUploads[i])); - } - } - - @Test - public void testIsSame() { - OCUpload upload1 = new OCUpload("/test", "/test", account.name); - upload1.setUseWifiOnly(true); - OCUpload upload2 = new OCUpload("/test", "/test", account.name); - upload2.setUseWifiOnly(true); - - assertTrue(upload1.isSame(upload2)); - - upload2.setUseWifiOnly(false); - assertFalse(upload1.isSame(upload2)); - - assertFalse(upload1.isSame(null)); - assertFalse(upload1.isSame(new OCFile("/test"))); - } - - private boolean contains(ArrayList uploads, OCUpload storedUpload) { - for (int i = 0; i < uploads.size(); i++) { - if (storedUpload.isSame(uploads.get(i), true)) { - return true; - } - } - return false; - } - - @Test(expected = IllegalArgumentException.class) - public void corruptedUpload() { - OCUpload corruptUpload = new OCUpload(File.separator + "LocalPath", - OCFile.PATH_SEPARATOR + "RemotePath", - account.name); - - corruptUpload.setLocalPath(null); - uploadsStorageManager.uploadDao.insertOrReplace(UploadEntityKt.toUploadEntity(corruptUpload)); - uploadsStorageManager.getAllStoredUploads(); - } - - @Test - public void getById() { - OCUpload upload = createUpload(account); - long id = uploadsStorageManager.uploadDao.insertOrReplace(UploadEntityKt.toUploadEntity(upload)); - OCUpload newUpload = uploadsStorageManager.getUploadById(id); - - assertNotNull(newUpload); - assertEquals(upload.getLocalAction(), newUpload.getLocalAction()); - assertEquals(upload.getFolderUnlockToken(), newUpload.getFolderUnlockToken()); - } - - @Test - public void getByIdNull() { - OCUpload newUpload = uploadsStorageManager.getUploadById(-1); - - assertNull(newUpload); - } - - private void insertUploads(Account account, int rowsToInsert) { - for (int i = 0; i < rowsToInsert; i++) { - uploadsStorageManager.uploadDao.insertOrReplace(UploadEntityKt.toUploadEntity(createUpload(account))); - } - } - - public String generateUniqueNumber() { - UUID uuid = UUID.randomUUID(); - return uuid.toString(); - } - - private OCUpload createUpload(Account account) { - OCUpload upload = new OCUpload(File.separator + "very long long long long long long long long long long long " + - "long long long long long long long long long long long long long long " + - "long long long long long long long long long long long long long long " + - "long long long long long long long LocalPath " + - generateUniqueNumber(), - OCFile.PATH_SEPARATOR + "very long long long long long long long long long " + - "long long long long long long long long long long long long long long " + - "long long long long long long long long long long long long long long " + - "long long long long long long long long long long long long RemotePath " + - generateUniqueNumber(), - account.name); - - upload.setFileSize(new Random().nextInt(20000) * 10000); - upload.setUploadStatus(UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS); - upload.setLocalAction(2); - upload.setNameCollisionPolicy(NameCollisionPolicy.ASK_USER); - upload.setCreateRemoteFolder(false); - upload.setUploadEndTimestamp(System.currentTimeMillis()); - upload.setLastResult(UploadResult.DELAYED_FOR_WIFI); - upload.setCreatedBy(UploadFileOperation.CREATED_BY_USER); - upload.setUseWifiOnly(true); - upload.setWhileChargingOnly(false); - upload.setFolderUnlockToken(RandomStringGenerator.make(10)); - - return upload; - } - - private void deleteAllUploads() { - uploadsStorageManager.removeAllUploads(); - - assertEquals(0, uploadsStorageManager.getAllStoredUploads().length); - } - - @After - public void tearDown() { - deleteAllUploads(); - userAccountManager.removeUser(user2); - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.kt b/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.kt new file mode 100644 index 000000000000..dd1b256c86d6 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.kt @@ -0,0 +1,239 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2017 JARP + * SPDX-FileCopyrightText: 2019 Tobias Kaminsky + * SPDX-FileCopyrightText: 2021 Chris Narkiewicz + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.owncloud.android.datamodel + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.ActivityNotFoundException +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.nextcloud.client.account.CurrentAccountProvider +import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.account.UserAccountManagerImpl +import com.nextcloud.client.database.entity.toUploadEntity +import com.nextcloud.test.RandomStringGenerator.make +import com.owncloud.android.AbstractIT +import com.owncloud.android.MainApp +import com.owncloud.android.db.OCUpload +import com.owncloud.android.db.UploadResult +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.lib.common.accounts.AccountUtils +import com.owncloud.android.operations.UploadFileOperation +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import java.io.File +import java.util.Random +import java.util.UUID +import java.util.function.Supplier + +/** + * Created by JARP on 6/7/17. + */ +@RunWith(AndroidJUnit4::class) +@SmallTest +class UploadStorageManagerTest : AbstractIT() { + private lateinit var uploadsStorageManager: UploadsStorageManager + + @Mock + private lateinit var currentAccountProvider: CurrentAccountProvider + + private lateinit var userAccountManager: UserAccountManager + + private lateinit var user2: User + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + + val instrumentationCtx = ApplicationProvider.getApplicationContext() + val contentResolver = instrumentationCtx.contentResolver + uploadsStorageManager = UploadsStorageManager(currentAccountProvider, contentResolver) + userAccountManager = UserAccountManagerImpl.fromContext(targetContext) + + val temp = Account("test2@test.com", MainApp.getAccountType(targetContext)) + if (!userAccountManager.exists(temp)) { + val platformAccountManager = AccountManager.get(targetContext) + platformAccountManager.addAccountExplicitly(temp, "testPassword", null) + platformAccountManager.setUserData( + temp, + AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION, + UserAccountManager.ACCOUNT_VERSION.toString() + ) + platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_VERSION, "14.0.0.0") + platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_BASE_URL, "test.com") + platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_USER_ID, "test") // same as userId + } + + val userAccountManager: UserAccountManager = UserAccountManagerImpl.fromContext(targetContext) + user2 = userAccountManager.getUser("test2@test.com") + .orElseThrow(Supplier { ActivityNotFoundException() }) + } + + @Test + fun testDeleteAllUploads() { + // Clean + for (user in userAccountManager.getAllUsers()) { + uploadsStorageManager.removeUserUploads(user) + } + val accountRowsA = 3 + val accountRowsB = 4 + insertUploads(account, accountRowsA) + insertUploads(user2.toPlatformAccount(), accountRowsB) + + Assert.assertEquals( + "Expected 4 removed uploads files", + 4, + uploadsStorageManager.removeUserUploads(user2).toLong() + ) + } + + @Test + fun largeTest() { + val size = 3000 + val uploads = ArrayList() + + deleteAllUploads() + Assert.assertEquals(0, uploadsStorageManager.getAllStoredUploads().size.toLong()) + + for (i in 0.., storedUpload: OCUpload): Boolean { + for (i in uploads.indices) { + if (storedUpload.isSame(uploads.get(i), true)) { + return true + } + } + return false + } + + @Test(expected = NullPointerException::class) + fun corruptedUpload() { + val corruptUpload = OCUpload( + File.separator + "LocalPath", + OCFile.PATH_SEPARATOR + "RemotePath", + account.name + ) + + corruptUpload.localPath = null + uploadsStorageManager.uploadDao.insertOrReplace(corruptUpload.toUploadEntity()) + uploadsStorageManager.getAllStoredUploads() + } + + @Test + fun getById() { + val upload = createUpload(account) + val id = uploadsStorageManager.uploadDao.insertOrReplace(upload.toUploadEntity()) + val newUpload = uploadsStorageManager.getUploadById(id) + + Assert.assertNotNull(newUpload) + Assert.assertEquals(upload.localAction.toLong(), newUpload!!.localAction.toLong()) + Assert.assertEquals(upload.folderUnlockToken, newUpload.folderUnlockToken) + } + + @Test + fun getByIdNull() { + val newUpload = uploadsStorageManager.getUploadById(-1) + Assert.assertNull(newUpload) + } + + private fun insertUploads(account: Account, rowsToInsert: Int) { + for (i in 0.. - * SPDX-FileCopyrightText: 2024 Jonas Mayer - * SPDX-FileCopyrightText: 2022 Álvaro Brey - * SPDX-FileCopyrightText: 2018-2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2019-2021 Chris Narkiewicz - * SPDX-FileCopyrightText: 2016-2020 Andy Scherzinger - * SPDX-FileCopyrightText: 2016 ownCloud Inc. - * SPDX-FileCopyrightText: 2016 María Asensio Valverde - * SPDX-FileCopyrightText: 2016 David A. Velasco - * SPDX-FileCopyrightText: 2014 Luke Owncloud - * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) - */ -package com.owncloud.android.datamodel; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.database.Cursor; -import android.os.Handler; -import android.os.Looper; - -import com.nextcloud.client.account.CurrentAccountProvider; -import com.nextcloud.client.account.User; -import com.nextcloud.client.database.NextcloudDatabase; -import com.nextcloud.client.database.dao.UploadDao; -import com.nextcloud.client.database.entity.UploadEntity; -import com.nextcloud.client.database.entity.UploadEntityKt; -import com.nextcloud.client.jobs.upload.FileUploadHelper; -import com.nextcloud.client.jobs.upload.FileUploadWorker; -import com.nextcloud.utils.autoRename.AutoRename; -import com.nextcloud.utils.extensions.RemoteOperationResultExtensionsKt; -import com.owncloud.android.MainApp; -import com.owncloud.android.db.OCUpload; -import com.owncloud.android.db.ProviderMeta.ProviderTableMeta; -import com.owncloud.android.db.UploadResult; -import com.owncloud.android.files.services.NameCollisionPolicy; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.status.OCCapability; -import com.owncloud.android.operations.UploadFileOperation; -import com.owncloud.android.utils.theme.CapabilityUtils; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.List; -import java.util.Locale; -import java.util.Observable; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -/** - * Database helper for storing list of files to be uploaded, including status information for each file. - */ -public class UploadsStorageManager extends Observable { - private static final String TAG = UploadsStorageManager.class.getSimpleName(); - - private static final String IS_EQUAL = "== ?"; - private static final String EQUAL = "=="; - private static final String OR = " OR "; - private static final String AND = " AND "; - private static final String ANGLE_BRACKETS = "<>"; - private static final int SINGLE_RESULT = 1; - - private static final long QUERY_PAGE_SIZE = 100; - - private final ContentResolver contentResolver; - private final CurrentAccountProvider currentAccountProvider; - private OCCapability capability; - public final UploadDao uploadDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).uploadDao(); - - public UploadsStorageManager( - CurrentAccountProvider currentAccountProvider, - ContentResolver contentResolver - ) { - if (contentResolver == null) { - throw new IllegalArgumentException("Cannot create an instance with a NULL contentResolver"); - } - this.contentResolver = contentResolver; - this.currentAccountProvider = currentAccountProvider; - } - - private void initOCCapability() { - try { - this.capability = CapabilityUtils.getCapability(MainApp.getAppContext()); - } catch (RuntimeException e) { - Log_OC.e(TAG,"Failed to set OCCapability: Dependencies are not yet ready."); - } - } - - /** - * Update an upload object in DB. - * - * @param ocUpload Upload object with state to update - * @return num of updated uploads. - */ - public synchronized int updateUpload(OCUpload ocUpload) { - Log_OC.v(TAG, "Updating " + ocUpload.getLocalPath() + " with status=" + ocUpload.getUploadStatus()); - - OCUpload existingUpload = getUploadById(ocUpload.getUploadId()); - if (existingUpload == null) { - Log_OC.e(TAG, "Upload not found for ID: " + ocUpload.getUploadId()); - return 0; - } - - if (!existingUpload.getAccountName().equals(ocUpload.getAccountName())) { - Log_OC.e(TAG, "Account mismatch for upload ID " + ocUpload.getUploadId() + - ": expected " + existingUpload.getAccountName() + - ", got " + ocUpload.getAccountName()); - return 0; - } - - ContentValues cv = new ContentValues(); - cv.put(ProviderTableMeta.UPLOADS_LOCAL_PATH, ocUpload.getLocalPath()); - cv.put(ProviderTableMeta.UPLOADS_REMOTE_PATH, ocUpload.getRemotePath()); - cv.put(ProviderTableMeta.UPLOADS_ACCOUNT_NAME, ocUpload.getAccountName()); - cv.put(ProviderTableMeta.UPLOADS_STATUS, ocUpload.getUploadStatus().value); - cv.put(ProviderTableMeta.UPLOADS_LAST_RESULT, ocUpload.getLastResult().getValue()); - - long uploadEndTimestamp = ocUpload.getUploadEndTimestamp(); - cv.put(ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP_LONG, uploadEndTimestamp); - cv.put(ProviderTableMeta.UPLOADS_FILE_SIZE, ocUpload.getFileSize()); - cv.put(ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN, ocUpload.getFolderUnlockToken()); - - int result = getDB().update(ProviderTableMeta.CONTENT_URI_UPLOADS, - cv, - ProviderTableMeta._ID + "=? AND " + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "=?", - new String[]{String.valueOf(ocUpload.getUploadId()), ocUpload.getAccountName()} - ); - - Log_OC.d(TAG, "updateUpload returns with: " + result + " for file: " + ocUpload.getLocalPath()); - if (result != SINGLE_RESULT) { - Log_OC.e(TAG, "Failed to update item " + ocUpload.getLocalPath() + " into upload db."); - } else { - notifyObserversNow(); - } - - return result; - } - - private int updateUploadInternal(Cursor c, UploadStatus status, UploadResult result, String remotePath, - String localPath) { - - int r = 0; - while (c.moveToNext()) { - // read upload object and update - OCUpload upload = createOCUploadFromCursor(c); - - String path = c.getString(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_LOCAL_PATH)); - Log_OC.v( - TAG, - "Updating " + path + " with status:" + status + " and result:" - + (result == null ? "null" : result.toString()) + " (old:" - + upload.toFormattedString() + ')'); - - upload.setUploadStatus(status); - upload.setLastResult(result); - upload.setRemotePath(remotePath); - if (localPath != null) { - upload.setLocalPath(localPath); - } - if (status == UploadStatus.UPLOAD_SUCCEEDED) { - upload.setUploadEndTimestamp(Calendar.getInstance().getTimeInMillis()); - } - - // store update upload object to db - r = updateUpload(upload); - - } - - return r; - } - - /** - * Update upload status of file uniquely referenced by id. - * - * @param id upload id. - * @param status new status. - * @param result new result of upload operation - * @param remotePath path of the file to upload in the ownCloud storage - * @param localPath path of the file to upload in the device storage - * @return 1 if file status was updated, else 0. - */ - private void updateUploadStatus(long id, UploadStatus status, UploadResult result, String remotePath, - String localPath) { - //Log_OC.v(TAG, "Updating "+filepath+" with uploadStatus="+status +" and result="+result); - - Cursor c = getDB().query( - ProviderTableMeta.CONTENT_URI_UPLOADS, - null, - ProviderTableMeta._ID + "=?", - new String[]{String.valueOf(id)}, - null - ); - - if (c != null) { - if (c.getCount() != SINGLE_RESULT) { - Log_OC.e(TAG, c.getCount() + " items for id=" + id - + " available in UploadDb. Expected 1. Failed to update upload db."); - } else { - updateUploadInternal(c, status, result, remotePath, localPath); - } - c.close(); - } else { - Log_OC.e(TAG, "Cursor is null"); - } - - } - - /** - * Should be called when some value of this DB was changed. All observers are informed. - */ - public void notifyObserversNow() { - Log_OC.d(TAG, "notifyObserversNow"); - new Handler(Looper.getMainLooper()).post(() -> { - setChanged(); - notifyObservers(); - }); - } - - /** - * Remove an upload from the uploads list, known its target account and remote path. - * - * @param upload Upload instance to remove from persisted storage. - * @return true when the upload was stored and could be removed. - */ - public int removeUpload(@Nullable OCUpload upload) { - if (upload == null) { - return 0; - } else { - return removeUpload(upload.getUploadId()); - } - } - - /** - * Remove an upload from the uploads list, known its target account and remote path. - * - * @param id to remove from persisted storage. - * @return true when the upload was stored and could be removed. - */ - public int removeUpload(long id) { - int result = getDB().delete( - ProviderTableMeta.CONTENT_URI_UPLOADS, - ProviderTableMeta._ID + "=?", - new String[]{Long.toString(id)} - ); - Log_OC.d(TAG, "delete returns " + result + " for upload with id " + id); - if (result > 0) { - notifyObserversNow(); - } - return result; - } - - /** - * Remove an upload from the uploads list, known its target account and remote path. - * - * @param accountName Name of the OC account target of the upload to remove. - * @param remotePath Absolute path in the OC account target of the upload to remove. - * @return true when one or more upload entries were removed - */ - public int removeUpload(String accountName, String remotePath) { - int result = getDB().delete( - ProviderTableMeta.CONTENT_URI_UPLOADS, - ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "=? AND " + ProviderTableMeta.UPLOADS_REMOTE_PATH + "=?", - new String[]{accountName, remotePath} - ); - Log_OC.d(TAG, "delete returns " + result + " for file " + remotePath + " in " + accountName); - if (result > 0) { - notifyObserversNow(); - } - return result; - } - - /** - * Remove all the uploads of a given account from the uploads list. - * - * @param accountName Name of the OC account target of the uploads to remove. - * @return true when one or more upload entries were removed - */ - public int removeUploads(String accountName) { - int result = getDB().delete( - ProviderTableMeta.CONTENT_URI_UPLOADS, - ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "=?", - new String[]{accountName} - ); - Log_OC.d(TAG, "delete returns " + result + " for uploads in " + accountName); - if (result > 0) { - notifyObserversNow(); - } - return result; - } - - public OCUpload[] getAllStoredUploads() { - return getUploads(null, (String[]) null); - } - - public @Nullable - OCUpload getUploadById(long id) { - OCUpload result = null; - Cursor cursor = getDB().query( - ProviderTableMeta.CONTENT_URI_UPLOADS, - null, - ProviderTableMeta._ID + "=?", - new String[]{Long.toString(id)}, - "_id ASC"); - - if (cursor != null) { - if (cursor.moveToFirst()) { - result = createOCUploadFromCursor(cursor); - } - } - Log_OC.d(TAG, "Retrieve job " + result + " for id " + id); - return result; - } - - public List getUploadsByIds(long[] uploadIds, String accountName) { - final List result = new ArrayList<>(); - - final List entities = uploadDao.getUploadsByIds(uploadIds, accountName); - entities.forEach(uploadEntity -> { - OCUpload ocUpload = createOCUploadFromEntity(uploadEntity); - if (ocUpload != null) { - result.add(ocUpload); - } - }); - - return result; - } - - private OCUpload[] getUploads(@Nullable String selection, @Nullable String... selectionArgs) { - final List uploads = new ArrayList<>(); - long page = 0; - long rowsRead; - long rowsTotal = 0; - long lastRowID = -1; - - do { - final List uploadsPage = getUploadPage(lastRowID, selection, selectionArgs); - rowsRead = uploadsPage.size(); - rowsTotal += rowsRead; - if (!uploadsPage.isEmpty()) { - lastRowID = uploadsPage.get(uploadsPage.size() - 1).getUploadId(); - } - Log_OC.v(TAG, String.format(Locale.ENGLISH, - "getUploads() got %d rows from page %d, %d rows total so far, last ID %d", - rowsRead, - page, - rowsTotal, - lastRowID - )); - uploads.addAll(uploadsPage); - page++; - } while (rowsRead > 0); - - - Log_OC.v(TAG, String.format(Locale.ENGLISH, - "getUploads() returning %d (%d) rows after reading %d pages", - rowsTotal, - uploads.size(), - page - )); - - - return uploads.toArray(new OCUpload[0]); - } - - @NonNull - private List getUploadPage(final long afterId, @Nullable String selection, @Nullable String... selectionArgs) { - return getUploadPage(QUERY_PAGE_SIZE, afterId, true, selection, selectionArgs); - } - - @NonNull - private List getUploadPage(long limit, final long afterId, final boolean descending, @Nullable String selection, @Nullable String... selectionArgs) { - List uploads = new ArrayList<>(); - String pageSelection = selection; - String[] pageSelectionArgs = selectionArgs; - - String idComparator; - String sortDirection; - if (descending) { - sortDirection = "DESC"; - idComparator = "<"; - } else { - sortDirection = "ASC"; - idComparator = ">"; - } - - if (afterId >= 0) { - if (selection != null) { - pageSelection = "(" + selection + ") AND _id " + idComparator + " ?"; - } else { - pageSelection = "_id " + idComparator + " ?"; - } - if (selectionArgs != null) { - pageSelectionArgs = Arrays.copyOf(selectionArgs, selectionArgs.length + 1); - } else { - pageSelectionArgs = new String[1]; - } - pageSelectionArgs[pageSelectionArgs.length - 1] = String.valueOf(afterId); - Log_OC.d(TAG, String.format(Locale.ENGLISH, "QUERY: %s ROWID: %d", pageSelection, afterId)); - } else { - Log_OC.d(TAG, String.format(Locale.ENGLISH, "QUERY: %s ROWID: %d", selection, afterId)); - } - - String sortOrder; - if (limit > 0) { - sortOrder = String.format(Locale.ENGLISH, "_id " + sortDirection + " LIMIT %d", limit); - } else { - sortOrder = String.format(Locale.ENGLISH, "_id " + sortDirection); - } - - Cursor c = getDB().query( - ProviderTableMeta.CONTENT_URI_UPLOADS, - null, - pageSelection, - pageSelectionArgs, - sortOrder); - - if (c != null) { - if (c.moveToFirst()) { - do { - OCUpload upload = createOCUploadFromCursor(c); - if (upload == null) { - Log_OC.e(TAG, "OCUpload could not be created from cursor"); - } else { - uploads.add(upload); - } - } while (c.moveToNext() && !c.isAfterLast()); - } - c.close(); - } - return uploads; - } - - @Nullable - private OCUpload createOCUploadFromEntity(UploadEntity entity) { - if (entity == null) { - return null; - } - initOCCapability(); - return UploadEntityKt.toOCUpload(entity, capability); - } - - private OCUpload createOCUploadFromCursor(Cursor c) { - initOCCapability(); - - OCUpload upload = null; - if (c != null) { - String localPath = c.getString(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_LOCAL_PATH)); - - String remotePath = c.getString(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_REMOTE_PATH)); - if (capability != null) { - remotePath = AutoRename.INSTANCE.rename(remotePath, capability); - } - - String accountName = c.getString(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_ACCOUNT_NAME)); - upload = new OCUpload(localPath, remotePath, accountName); - - upload.setFileSize(c.getLong(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_FILE_SIZE))); - upload.setUploadId(c.getLong(c.getColumnIndexOrThrow(ProviderTableMeta._ID))); - upload.setUploadStatus( - UploadStatus.fromValue(c.getInt(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_STATUS))) - ); - upload.setLocalAction(c.getInt(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR))); - upload.setNameCollisionPolicy(NameCollisionPolicy.deserialize(c.getInt( - c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY)))); - upload.setCreateRemoteFolder(c.getInt( - c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER)) == 1); - - final var uploadEndTimestampColumnIndex= c.getColumnIndex(ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP_LONG); - if (uploadEndTimestampColumnIndex > -1) { - final var uploadEndTimestamp = c.getLong(uploadEndTimestampColumnIndex); - if (uploadEndTimestamp > 0) { - upload.setUploadEndTimestamp(uploadEndTimestamp); - } - } - upload.setLastResult(UploadResult.fromValue( - c.getInt(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_LAST_RESULT)))); - upload.setCreatedBy(c.getInt(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_CREATED_BY))); - upload.setUseWifiOnly(c.getInt(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_IS_WIFI_ONLY)) == 1); - upload.setWhileChargingOnly(c.getInt(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY)) - == 1); - upload.setFolderUnlockToken(c.getString(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN))); - } - return upload; - } - - public long[] getCurrentUploadIds(final @NonNull String accountName) { - final var result = uploadDao.getAllIds(UploadStatus.UPLOAD_IN_PROGRESS.value, accountName); - return result.stream() - .mapToLong(Integer::longValue) - .toArray(); - } - - public OCUpload[] getUploadsForAccount(final @NonNull String accountName) { - return getUploads(ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, accountName); - } - - private ContentResolver getDB() { - return contentResolver; - } - - public void clearFailedButNotDelayedUploads() { - User user = currentAccountProvider.getUser(); - final long deleted = getDB().delete( - ProviderTableMeta.CONTENT_URI_UPLOADS, - ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_FAILED.value + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - ANGLE_BRACKETS + UploadResult.LOCK_FAILED.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - ANGLE_BRACKETS + UploadResult.DELAYED_FOR_WIFI.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - ANGLE_BRACKETS + UploadResult.DELAYED_FOR_CHARGING.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - ANGLE_BRACKETS + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() + - AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, - new String[]{user.getAccountName()} - ); - Log_OC.d(TAG, "delete all failed uploads but those delayed for Wifi"); - if (deleted > 0) { - notifyObserversNow(); - } - } - - public void clearCancelledUploadsForCurrentAccount() { - User user = currentAccountProvider.getUser(); - final long deleted = getDB().delete( - ProviderTableMeta.CONTENT_URI_UPLOADS, - ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_CANCELLED.value + AND + - ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, new String[]{user.getAccountName()} - ); - - Log_OC.d(TAG, "delete all cancelled uploads"); - if (deleted > 0) { - notifyObserversNow(); - } - } - - public void clearSuccessfulUploads() { - User user = currentAccountProvider.getUser(); - final long deleted = getDB().delete( - ProviderTableMeta.CONTENT_URI_UPLOADS, - ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_SUCCEEDED.value + AND + - ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, new String[]{user.getAccountName()} - ); - - Log_OC.d(TAG, "delete all successful uploads"); - if (deleted > 0) { - notifyObserversNow(); - } - } - - public void updateDatabaseUploadResult(RemoteOperationResult uploadResult, UploadFileOperation upload) { - Log_OC.d(TAG, "updateDatabaseUploadResult uploadResult: " + uploadResult + " upload: " + upload); - - if (uploadResult.isCancelled()) { - Log_OC.w(TAG, "upload is cancelled, removing upload"); - removeUpload(upload.getUser().getAccountName(), upload.getRemotePath()); - return; - } - - String localPath = (upload.getLocalBehaviour() == FileUploadWorker.LOCAL_BEHAVIOUR_MOVE) - ? upload.getStoragePath() : null; - - - Log_OC.d(TAG, "local behaviour: " + upload.getLocalBehaviour()); - Log_OC.d(TAG, "local path of upload: " + localPath); - - UploadStatus status = UploadStatus.UPLOAD_FAILED; - UploadResult result = UploadResult.fromOperationResult(uploadResult); - RemoteOperationResult.ResultCode code = uploadResult.getCode(); - - if (uploadResult.isSuccess()) { - status = UploadStatus.UPLOAD_SUCCEEDED; - result = UploadResult.UPLOADED; - } else if (RemoteOperationResultExtensionsKt.isConflict(code)) { - boolean isSame = new FileUploadHelper().isSameFileOnRemote( - upload.getUser(), new File(upload.getStoragePath()), upload.getRemotePath(), upload.getContext()); - - if (isSame) { - result = UploadResult.SAME_FILE_CONFLICT; - status = UploadStatus.UPLOAD_SUCCEEDED; - } else { - result = UploadResult.SYNC_CONFLICT; - } - } else if (code == RemoteOperationResult.ResultCode.LOCAL_FILE_NOT_FOUND) { - // upload status is SUCCEEDED because user cannot take action about it, it will always fail - status = UploadStatus.UPLOAD_SUCCEEDED; - result = UploadResult.FILE_NOT_FOUND; - } - - Log_OC.d(TAG, String.format( - "Upload Finished [%s] | RemoteCode: %s | internalResult: %s | FinalStatus: %s | Path: %s", - uploadResult.isSuccess() ? "✅" : "❌", - code, - result.name(), - status, - upload.getRemotePath())); - updateUploadStatus(upload.getOCUploadId(), status, result, upload.getRemotePath(), localPath); - } - - /** - * Updates the persistent upload database with an upload now in progress. - */ - public void updateDatabaseUploadStart(UploadFileOperation upload) { - String localPath = (FileUploadWorker.LOCAL_BEHAVIOUR_MOVE == upload.getLocalBehaviour()) - ? upload.getStoragePath() : null; - - updateUploadStatus( - upload.getOCUploadId(), - UploadStatus.UPLOAD_IN_PROGRESS, - UploadResult.UNKNOWN, - upload.getRemotePath(), - localPath - ); - } - - @VisibleForTesting - public void removeAllUploads() { - Log_OC.v(TAG, "Delete all uploads!"); - getDB().delete( - ProviderTableMeta.CONTENT_URI_UPLOADS, - "", - new String[]{}); - } - - public int removeUserUploads(User user) { - Log_OC.v(TAG, "Delete all uploads for account " + user.getAccountName()); - return getDB().delete( - ProviderTableMeta.CONTENT_URI_UPLOADS, - ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "=?", - new String[]{user.getAccountName()}); - } - - public enum UploadStatus { - - /** - * Upload currently in progress or scheduled to be executed. - */ - UPLOAD_IN_PROGRESS(0), - - /** - * Last upload failed. - */ - UPLOAD_FAILED(1), - - /** - * Upload was successful. - */ - UPLOAD_SUCCEEDED(2), - - /** - * Upload was cancelled by the user. - */ - UPLOAD_CANCELLED(3); - - private final int value; - - UploadStatus(int value) { - this.value = value; - } - - public static UploadStatus fromValue(int value) { - return switch (value) { - case 0 -> UPLOAD_IN_PROGRESS; - case 1 -> UPLOAD_FAILED; - case 2 -> UPLOAD_SUCCEEDED; - case 3 -> UPLOAD_CANCELLED; - default -> null; - }; - } - - public int getValue() { - return value; - } - - } -} diff --git a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt new file mode 100644 index 000000000000..fb6ce25cfe7f --- /dev/null +++ b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt @@ -0,0 +1,595 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Jonas Mayer + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2018-2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2019-2021 Chris Narkiewicz + * SPDX-FileCopyrightText: 2016-2020 Andy Scherzinger + * SPDX-FileCopyrightText: 2016 ownCloud Inc. + * SPDX-FileCopyrightText: 2016 María Asensio Valverde + * SPDX-FileCopyrightText: 2016 David A. Velasco + * SPDX-FileCopyrightText: 2014 Luke Owncloud + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) + */ +package com.owncloud.android.datamodel + +import android.content.ContentResolver +import android.content.ContentValues +import android.database.Cursor +import android.os.Handler +import android.os.Looper +import androidx.annotation.VisibleForTesting +import com.nextcloud.client.account.CurrentAccountProvider +import com.nextcloud.client.account.User +import com.nextcloud.client.database.NextcloudDatabase +import com.nextcloud.client.database.dao.UploadDao +import com.nextcloud.client.database.entity.UploadEntity +import com.nextcloud.client.database.entity.toOCUpload +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.utils.autoRename.AutoRename +import com.nextcloud.utils.extensions.isConflict +import com.owncloud.android.MainApp +import com.owncloud.android.db.OCUpload +import com.owncloud.android.db.ProviderMeta.ProviderTableMeta +import com.owncloud.android.db.UploadResult +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.status.OCCapability +import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.utils.theme.CapabilityUtils +import java.io.File +import java.util.Calendar +import java.util.Locale +import java.util.Observable + +@Suppress("TooManyFunctions", "TooGenericExceptionCaught", "MagicNumber", "ReturnCount") +class UploadsStorageManager( + private val currentAccountProvider: CurrentAccountProvider, + private val contentResolver: ContentResolver +) : Observable() { + + private var capability: OCCapability? = null + + @JvmField + val uploadDao: UploadDao = NextcloudDatabase.instance().uploadDao() + + private fun initOCCapability() { + try { + this.capability = CapabilityUtils.getCapability(MainApp.getAppContext()) + } catch (e: RuntimeException) { + Log_OC.e(TAG, "Failed to set OCCapability: Dependencies are not yet ready. $e") + } + } + + @Synchronized + fun updateUpload(ocUpload: OCUpload): Int { + val existingUpload = getUploadById(ocUpload.uploadId) + if (existingUpload == null) { + Log_OC.e(TAG, "Upload not found for ID: " + ocUpload.uploadId) + return 0 + } + + if (existingUpload.accountName != ocUpload.accountName) { + Log_OC.e( + TAG, + "Account mismatch for upload ID " + ocUpload.uploadId + + ": expected " + existingUpload.accountName + + ", got " + ocUpload.accountName + ) + return 0 + } + + Log_OC.v(TAG, "Updating " + ocUpload.localPath + " with status=" + ocUpload.uploadStatus) + + val cv = ContentValues().apply { + put(ProviderTableMeta.UPLOADS_LOCAL_PATH, ocUpload.localPath) + put(ProviderTableMeta.UPLOADS_REMOTE_PATH, ocUpload.remotePath) + put(ProviderTableMeta.UPLOADS_ACCOUNT_NAME, ocUpload.accountName) + put(ProviderTableMeta.UPLOADS_STATUS, ocUpload.uploadStatus.value) + put(ProviderTableMeta.UPLOADS_LAST_RESULT, ocUpload.lastResult.value) + + val uploadEndTimestamp = ocUpload.uploadEndTimestamp + put(ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP_LONG, uploadEndTimestamp) + put(ProviderTableMeta.UPLOADS_FILE_SIZE, ocUpload.fileSize) + put(ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN, ocUpload.folderUnlockToken) + } + + val result = contentResolver.update( + ProviderTableMeta.CONTENT_URI_UPLOADS, + cv, + ProviderTableMeta._ID + "=? AND " + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "=?", + arrayOf(ocUpload.uploadId.toString(), ocUpload.accountName) + ) + + Log_OC.d(TAG, "updateUpload returns with: " + result + " for file: " + ocUpload.localPath) + + if (result != SINGLE_RESULT) { + Log_OC.e(TAG, "Failed to update item " + ocUpload.localPath + " into upload db.") + } else { + notifyObserversNow() + } + + return result + } + + private fun updateUploadInternal( + c: Cursor, + status: UploadStatus?, + result: UploadResult?, + remotePath: String?, + localPath: String? + ): Int { + var r = 0 + while (c.moveToNext()) { + val upload = createOCUploadFromCursor(c) + + val path = c.getString(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_LOCAL_PATH)) + Log_OC.v( + TAG, + ( + "Updating " + path + " with status:" + status + " and result:" + + (result?.toString() ?: "null") + " (old:" + + upload.toFormattedString() + ')' + ) + ) + + upload.setUploadStatus(status) + upload.lastResult = result + upload.remotePath = remotePath + + if (localPath != null) { + upload.localPath = localPath + } + + if (status == UploadStatus.UPLOAD_SUCCEEDED) { + upload.uploadEndTimestamp = Calendar.getInstance().getTimeInMillis() + } + + r = updateUpload(upload) + } + + return r + } + + private fun updateUploadStatus( + id: Long, + status: UploadStatus?, + result: UploadResult?, + remotePath: String?, + localPath: String? + ) { + val c = contentResolver.query( + ProviderTableMeta.CONTENT_URI_UPLOADS, + null, + ProviderTableMeta._ID + "=?", + arrayOf(id.toString()), + null + ) + + if (c != null) { + if (c.count != SINGLE_RESULT) { + Log_OC.e( + TAG, + ( + c.count.toString() + " items for id=" + id + + " available in UploadDb. Expected 1. Failed to update upload db." + ) + ) + } else { + updateUploadInternal(c, status, result, remotePath, localPath) + } + c.close() + } else { + Log_OC.e(TAG, "Cursor is null") + } + } + + fun notifyObserversNow() { + Log_OC.d(TAG, "notifying upload storage manager observers") + Handler(Looper.getMainLooper()).post { + setChanged() + notifyObservers() + } + } + + fun removeUpload(upload: OCUpload?): Int = if (upload == null) 0 else removeUpload(upload.uploadId) + + fun removeUpload(id: Long): Int { + val result = contentResolver.delete( + ProviderTableMeta.CONTENT_URI_UPLOADS, + ProviderTableMeta._ID + "=?", + arrayOf(id.toString()) + ) + Log_OC.d(TAG, "delete returns $result for upload with id $id") + if (result > 0) { + notifyObserversNow() + } + return result + } + + private fun removeUpload(accountName: String?, remotePath: String?): Int { + val result = contentResolver.delete( + ProviderTableMeta.CONTENT_URI_UPLOADS, + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "=? AND " + ProviderTableMeta.UPLOADS_REMOTE_PATH + "=?", + arrayOf(accountName, remotePath) + ) + Log_OC.d(TAG, "delete returns $result for file $remotePath in $accountName") + if (result > 0) { + notifyObserversNow() + } + return result + } + + fun removeUploads(accountName: String?): Int { + val result = contentResolver.delete( + ProviderTableMeta.CONTENT_URI_UPLOADS, + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "=?", + arrayOf(accountName) + ) + Log_OC.d(TAG, "delete returns $result for uploads in $accountName") + if (result > 0) { + notifyObserversNow() + } + return result + } + + fun getAllStoredUploads(): Array = getUploads(null) + + fun getUploadById(id: Long): OCUpload? { + var result: OCUpload? = null + val cursor = contentResolver.query( + ProviderTableMeta.CONTENT_URI_UPLOADS, + null, + ProviderTableMeta._ID + "=?", + arrayOf(id.toString()), + "_id ASC" + ) + + if (cursor != null) { + if (cursor.moveToFirst()) { + result = createOCUploadFromCursor(cursor) + } + } + Log_OC.d(TAG, "Retrieve job $result for id $id") + return result + } + + fun getUploadsByIds(uploadIds: LongArray, accountName: String): List { + val result = ArrayList() + uploadDao.getUploadsByIds(uploadIds, accountName).forEach { entity -> + createOCUploadFromEntity(entity)?.let { result.add(it) } + } + return result + } + + private fun getUploads(selection: String?, vararg selectionArgs: String?): Array { + val uploads = ArrayList() + var page: Long = 0 + var rowsRead: Long + var rowsTotal: Long = 0 + var lastRowID: Long = -1 + + do { + val uploadsPage = getUploadPage(lastRowID, selection, *selectionArgs) + rowsRead = uploadsPage.size.toLong() + rowsTotal += rowsRead + if (uploadsPage.isNotEmpty()) { + lastRowID = uploadsPage.last().uploadId + } + Log_OC.v( + TAG, + String.format( + Locale.ENGLISH, + "getUploads() got %d rows from page %d, %d rows total so far, last ID %d", + rowsRead, + page, + rowsTotal, + lastRowID + ) + ) + uploads.addAll(uploadsPage) + page++ + } while (rowsRead > 0) + + Log_OC.v( + TAG, + String.format( + Locale.ENGLISH, + "getUploads() returning %d (%d) rows after reading %d pages", + rowsTotal, + uploads.size, + page + ) + ) + + return uploads.toTypedArray() + } + + private fun getUploadPage(afterId: Long, selection: String?, vararg selectionArgs: String?): List = + getUploadPage(QUERY_PAGE_SIZE, afterId, true, selection, *selectionArgs) + + private fun getUploadPage( + limit: Long, + afterId: Long, + descending: Boolean, + selection: String?, + vararg selectionArgs: String? + ): List { + val uploads = ArrayList() + val (sortDirection, idComparator) = if (descending) "DESC" to "<" else "ASC" to ">" + val pageSelection: String? + val pageSelectionArgs: Array + + if (afterId >= 0) { + pageSelection = if (selection != null) "($selection) AND _id $idComparator ?" else "_id $idComparator ?" + pageSelectionArgs = arrayOfNulls(selectionArgs.size + 1).also { arr -> + selectionArgs.forEachIndexed { i, v -> arr[i] = v } + arr[selectionArgs.size] = afterId.toString() + } + Log_OC.d(TAG, String.format(Locale.ENGLISH, "QUERY: %s ROWID: %d", pageSelection, afterId)) + } else { + pageSelection = selection + pageSelectionArgs = arrayOfNulls(selectionArgs.size).also { arr -> + selectionArgs.forEachIndexed { i, v -> arr[i] = v } + } + Log_OC.d(TAG, String.format(Locale.ENGLISH, "QUERY: %s ROWID: %d", selection, afterId)) + } + + val sortOrder = if (limit > 0) { + String.format(Locale.ENGLISH, "_id $sortDirection LIMIT %d", limit) + } else { + "_id $sortDirection" + } + + contentResolver.query( + ProviderTableMeta.CONTENT_URI_UPLOADS, + null, + pageSelection, + pageSelectionArgs, + sortOrder + )?.use { c -> + if (c.moveToFirst()) { + do { + uploads.add(createOCUploadFromCursor(c)) + } while (c.moveToNext() && !c.isAfterLast) + } + } + + return uploads + } + + private fun createOCUploadFromEntity(entity: UploadEntity?): OCUpload? { + if (entity == null) return null + initOCCapability() + return entity.toOCUpload(capability) + } + + private fun createOCUploadFromCursor(c: Cursor): OCUpload { + initOCCapability() + + fun Cursor.str(col: String): String = getString(getColumnIndexOrThrow(col)) + fun Cursor.int(col: String): Int = getInt(getColumnIndexOrThrow(col)) + fun Cursor.long(col: String): Long = getLong(getColumnIndexOrThrow(col)) + + var remotePath = c.str(ProviderTableMeta.UPLOADS_REMOTE_PATH) + if (capability != null) { + remotePath = AutoRename.rename(remotePath, capability!!) + } + + return OCUpload( + c.str(ProviderTableMeta.UPLOADS_LOCAL_PATH), + remotePath, + c.str(ProviderTableMeta.UPLOADS_ACCOUNT_NAME) + ).apply { + fileSize = c.long(ProviderTableMeta.UPLOADS_FILE_SIZE) + uploadId = c.long(ProviderTableMeta._ID) + setUploadStatus(UploadStatus.fromValue(c.int(ProviderTableMeta.UPLOADS_STATUS))) + localAction = c.int(ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR) + nameCollisionPolicy = + NameCollisionPolicy.deserialize(c.int(ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY)) + isCreateRemoteFolder = c.int(ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER) == 1 + + val timestampIndex = c.getColumnIndex(ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP_LONG) + if (timestampIndex > -1) { + val ts = c.getLong(timestampIndex) + if (ts > 0) uploadEndTimestamp = ts + } + + lastResult = UploadResult.fromValue(c.int(ProviderTableMeta.UPLOADS_LAST_RESULT)) + createdBy = c.int(ProviderTableMeta.UPLOADS_CREATED_BY) + isUseWifiOnly = c.int(ProviderTableMeta.UPLOADS_IS_WIFI_ONLY) == 1 + isWhileChargingOnly = c.int(ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY) == 1 + folderUnlockToken = c.str(ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN) + } + } + + fun getCurrentUploadIds(accountName: String): LongArray = + uploadDao.getAllIds(UploadStatus.UPLOAD_IN_PROGRESS.value, accountName) + .stream() + .mapToLong { it.toLong() } + .toArray() + + fun getUploadsForAccount(accountName: String): Array = + getUploads(ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, accountName) + + fun clearFailedButNotDelayedUploads() { + val user = currentAccountProvider.user + val deleted = contentResolver.delete( + ProviderTableMeta.CONTENT_URI_UPLOADS, + ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_FAILED.value + + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + ANGLE_BRACKETS + UploadResult.LOCK_FAILED.value + + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + ANGLE_BRACKETS + UploadResult.DELAYED_FOR_WIFI.value + + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + ANGLE_BRACKETS + UploadResult.DELAYED_FOR_CHARGING.value + + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + ANGLE_BRACKETS + + UploadResult.DELAYED_IN_POWER_SAVE_MODE.value + + AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, + arrayOf(user.accountName) + ) + + Log_OC.d(TAG, "delete all failed uploads but those delayed for Wifi") + + if (deleted > 0) notifyObserversNow() + } + + fun clearCancelledUploadsForCurrentAccount() { + val user = currentAccountProvider.user + val deleted = contentResolver.delete( + ProviderTableMeta.CONTENT_URI_UPLOADS, + ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_CANCELLED.value + + AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, + arrayOf(user.accountName) + ) + Log_OC.d(TAG, "delete all cancelled uploads") + if (deleted > 0) notifyObserversNow() + } + + fun clearSuccessfulUploads() { + val user = currentAccountProvider.user + val deleted = contentResolver.delete( + ProviderTableMeta.CONTENT_URI_UPLOADS, + ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_SUCCEEDED.value + + AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, + arrayOf(user.accountName) + ) + Log_OC.d(TAG, "delete all successful uploads") + if (deleted > 0) notifyObserversNow() + } + + fun updateDatabaseUploadResult(uploadResult: RemoteOperationResult<*>, upload: UploadFileOperation) { + Log_OC.d(TAG, "updateDatabaseUploadResult uploadResult: $uploadResult upload: $upload") + + if (uploadResult.isCancelled) { + Log_OC.w(TAG, "upload is cancelled, removing upload") + removeUpload(upload.user.accountName, upload.remotePath) + return + } + + val localPath = + if (upload.localBehaviour == FileUploadWorker.LOCAL_BEHAVIOUR_MOVE) upload.storagePath else null + + Log_OC.d(TAG, "local behaviour: " + upload.localBehaviour) + Log_OC.d(TAG, "local path of upload: $localPath") + + var status = UploadStatus.UPLOAD_FAILED + var result = UploadResult.fromOperationResult(uploadResult) + val code = uploadResult.code + + if (uploadResult.isSuccess) { + status = UploadStatus.UPLOAD_SUCCEEDED + result = UploadResult.UPLOADED + } else if (code.isConflict()) { + val isSame = FileUploadHelper().isSameFileOnRemote( + upload.user, + File(upload.storagePath), + upload.remotePath, + upload.context + ) + + if (isSame) { + result = UploadResult.SAME_FILE_CONFLICT + status = UploadStatus.UPLOAD_SUCCEEDED + } else { + result = UploadResult.SYNC_CONFLICT + } + } else if (code == RemoteOperationResult.ResultCode.LOCAL_FILE_NOT_FOUND) { + // upload status is SUCCEEDED because user cannot take action about it, it will always fail + status = UploadStatus.UPLOAD_SUCCEEDED + result = UploadResult.FILE_NOT_FOUND + } + + Log_OC.d( + TAG, + String.format( + "Upload Finished [%s] | RemoteCode: %s | internalResult: %s | FinalStatus: %s | Path: %s", + if (uploadResult.isSuccess) "✅" else "❌", + code, + result.name, + status, + upload.remotePath + ) + ) + + updateUploadStatus(upload.ocUploadId, status, result, upload.remotePath, localPath) + } + + fun updateDatabaseUploadStart(upload: UploadFileOperation) { + val localPath = + if (FileUploadWorker.LOCAL_BEHAVIOUR_MOVE == upload.localBehaviour) upload.storagePath else null + + updateUploadStatus( + upload.ocUploadId, + UploadStatus.UPLOAD_IN_PROGRESS, + UploadResult.UNKNOWN, + upload.remotePath, + localPath + ) + } + + @VisibleForTesting + fun removeAllUploads() { + Log_OC.v(TAG, "Delete all uploads!") + contentResolver.delete( + ProviderTableMeta.CONTENT_URI_UPLOADS, + "", + arrayOf() + ) + } + + fun removeUserUploads(user: User): Int { + Log_OC.v(TAG, "Delete all uploads for account " + user.accountName) + return contentResolver.delete( + ProviderTableMeta.CONTENT_URI_UPLOADS, + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "=?", + arrayOf(user.accountName) + ) + } + + enum class UploadStatus(val value: Int) { + /** + * Upload currently in progress or scheduled to be executed. + */ + UPLOAD_IN_PROGRESS(0), + + /** + * Last upload failed. + */ + UPLOAD_FAILED(1), + + /** + * Upload was successful. + */ + UPLOAD_SUCCEEDED(2), + + /** + * Upload was cancelled by the user. + */ + UPLOAD_CANCELLED(3); + + companion object { + fun fromValue(value: Int): UploadStatus? = when (value) { + 0 -> UPLOAD_IN_PROGRESS + 1 -> UPLOAD_FAILED + 2 -> UPLOAD_SUCCEEDED + 3 -> UPLOAD_CANCELLED + else -> null + } + } + } + + companion object { + private val TAG: String = UploadsStorageManager::class.java.getSimpleName() + + private const val IS_EQUAL = "== ?" + private const val EQUAL = "==" + private const val OR = " OR " + private const val AND = " AND " + private const val ANGLE_BRACKETS = "<>" + private const val SINGLE_RESULT = 1 + + private const val QUERY_PAGE_SIZE: Long = 100 + } +}