diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 6a3ce2ebc4..1f99e39fed 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -28,12 +28,12 @@ jobs: echo "::set-output name=pr::${{ github.event.pull_request.number }}" echo "::set-output name=repo::${{ github.event.pull_request.head.repo.full_name }}" fi - - uses: actions/checkout@v3 + - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 with: repository: ${{ steps.get-vars.outputs.repo }} ref: ${{ steps.get-vars.outputs.branch }} - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@1df8dbefe2a8cbc99770194893dd902763bee34b # v3 with: distribution: "temurin" java-version: 11 diff --git a/.github/workflows/assembleFlavors.yml b/.github/workflows/assembleFlavors.yml index 8979c1442f..6bec7e9545 100644 --- a/.github/workflows/assembleFlavors.yml +++ b/.github/workflows/assembleFlavors.yml @@ -15,9 +15,9 @@ jobs: matrix: flavor: [ Generic, Gplay, Huawei ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 - name: set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@1df8dbefe2a8cbc99770194893dd902763bee34b # v3 with: distribution: "temurin" java-version: 11 diff --git a/.github/workflows/autoApproveDependabot.yml b/.github/workflows/autoApproveDependabot.yml index 1946010fd3..8deb91bf16 100644 --- a/.github/workflows/autoApproveDependabot.yml +++ b/.github/workflows/autoApproveDependabot.yml @@ -10,7 +10,7 @@ jobs: auto-approve: runs-on: ubuntu-latest steps: - - uses: hmarr/auto-approve-action@v3.1.0 + - uses: hmarr/auto-approve-action@de8ae18c173c131e182d4adf2c874d8d2308a85b # v3.1.0 if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' with: github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0c38dfa7c3..63f970d6ca 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -15,9 +15,9 @@ jobs: matrix: task: [ detekt, spotlessKotlinCheck ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@1df8dbefe2a8cbc99770194893dd902763bee34b # v3 with: distribution: "temurin" java-version: 11 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fb9b2f62a8..c68c81b46f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -23,27 +23,27 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'java' ] + language: [ 'java' ] steps: - - name: Checkout repository - uses: actions/checkout@v3 - - name: Set Swap Space - uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c #v1.0 - with: + - name: Checkout repository + uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 + - name: Set Swap Space + uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0 + with: swap-size-gb: 10 - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - - name: Set up JDK - uses: actions/setup-java@v3 - with: - distribution: "temurin" - java-version: 11 - - name: Assemble - run: | - mkdir -p "$HOME/.gradle" - echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties" - ./gradlew assembleDebug - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + - name: Initialize CodeQL + uses: github/codeql-action/init@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2 + with: + languages: ${{ matrix.language }} + - name: Set up JDK + uses: actions/setup-java@1df8dbefe2a8cbc99770194893dd902763bee34b # v3 + with: + distribution: "temurin" + java-version: 11 + - name: Assemble + run: | + mkdir -p "$HOME/.gradle" + echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties" + ./gradlew assembleDebug + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2 diff --git a/.github/workflows/detectNewJavaFiles.yml b/.github/workflows/detectNewJavaFiles.yml index 7107619e4e..b64964cef7 100644 --- a/.github/workflows/detectNewJavaFiles.yml +++ b/.github/workflows/detectNewJavaFiles.yml @@ -1,3 +1,4 @@ +# synced from @nextcloud/android-config name: "Detect new java files" on: @@ -10,23 +11,22 @@ jobs: detectNewJavaFiles: runs-on: ubuntu-latest steps: - - id: file_changes - uses: trilom/file-changes-action@v1.2.4 - with: - output: ',' - - name: Detect new java files - run: | - if [ -z '${{ steps.file_changes.outputs.files_added }}' ]; then - echo "No new files added" - exit 0 - fi - new_java=$(echo '${{ steps.file_changes.outputs.files_added }}' | tr ',' '\n' | grep '\.java$' | cat) - if [ -n "$new_java" ]; then - # shellcheck disable=SC2016 - printf 'New java files detected:\n```\n%s\n```\n' "$new_java" | tee "$GITHUB_STEP_SUMMARY" - exit 1 - else - echo "No new java files detected" - exit 0 - fi - + - id: file_changes + uses: trilom/file-changes-action@a6ca26c14274c33b15e6499323aac178af06ad4b # v1.2.4 + with: + output: ',' + - name: Detect new java files + run: | + if [ -z '${{ steps.file_changes.outputs.files_added }}' ]; then + echo "No new files added" + exit 0 + fi + new_java=$(echo '${{ steps.file_changes.outputs.files_added }}' | tr ',' '\n' | grep '\.java$' | cat) + if [ -n "$new_java" ]; then + # shellcheck disable=SC2016 + printf 'New java files detected:\n```\n%s\n```\n' "$new_java" | tee "$GITHUB_STEP_SUMMARY" + exit 1 + else + echo "No new java files detected" + exit 0 + fi diff --git a/.github/workflows/detectSnapshot.yml b/.github/workflows/detectSnapshot.yml index c529840486..d1c3c754ed 100644 --- a/.github/workflows/detectSnapshot.yml +++ b/.github/workflows/detectSnapshot.yml @@ -12,6 +12,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 - name: Detect SNAPSHOT run: scripts/analysis/detectSNAPSHOT.sh diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index d0d8f814e2..0e649b2e9e 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -12,5 +12,5 @@ jobs: name: "Validation" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: gradle/wrapper-validation-action@v1 + - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 + - uses: gradle/wrapper-validation-action@55e685c48d84285a5b0418cd094606e199cca3b6 # v1 diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 59c3b803ab..7c228a5d0a 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -15,10 +15,10 @@ jobs: - name: Check if secrets are available run: echo "::set-output name=ok::${{ secrets.KS_PASS != '' }}" id: check-secrets - - uses: actions/checkout@v3 + - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 if: ${{ steps.check-secrets.outputs.ok == 'true' }} - name: set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@1df8dbefe2a8cbc99770194893dd902763bee34b # v3 if: ${{ steps.check-secrets.outputs.ok == 'true' }} with: distribution: "temurin" diff --git a/.github/workflows/screenShotTest.yml b/.github/workflows/screenShotTest.yml index ac36160e9c..74b646102b 100644 --- a/.github/workflows/screenShotTest.yml +++ b/.github/workflows/screenShotTest.yml @@ -18,17 +18,17 @@ jobs: color: [ blue ] api-level: [ 27 ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 - name: Gradle cache - uses: actions/cache@v3 + uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 # v3 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }} - name: AVD cache - uses: actions/cache@v3 + uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 # v3 id: avd-cache with: path: | @@ -36,14 +36,14 @@ jobs: ~/.android/adb* key: avd-${{ matrix.api-level }} - - uses: actions/setup-java@v3 + - uses: actions/setup-java@1df8dbefe2a8cbc99770194893dd902763bee34b # v3 with: distribution: "temurin" java-version: 11 - name: create AVD and generate snapshot for caching if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@50986b1464923454c95e261820bc626f38490ec0 # v2 with: api-level: ${{ matrix.api-level }} force-avd-creation: false @@ -64,12 +64,12 @@ jobs: - name: Delete old comments env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} if: ${{ always() }} run: scripts/deleteOldComments.sh "${{ matrix.color }}-${{ matrix.scheme }}" "Screenshot" ${{github.event.number}} - name: Run screenshot tests - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@50986b1464923454c95e261820bc626f38490ec0 # v2 with: api-level: ${{ matrix.api-level }} force-avd-creation: false @@ -82,8 +82,7 @@ jobs: if: ${{ failure() }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: - scripts/uploadReport.sh "${{ secrets.LOG_USERNAME }}" "${{ secrets.LOG_PASSWORD }}" ${{github.event.number}} "${{ matrix.color }}-${{ matrix.scheme }}" "Screenshot" ${{github.event.number}} + run: scripts/uploadReport.sh "${{ secrets.LOG_USERNAME }}" "${{ secrets.LOG_PASSWORD }}" ${{github.event.number}} "${{ matrix.color }}-${{ matrix.scheme }}" "Screenshot" ${{github.event.number}} - name: Archive Espresso results uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb if: ${{ always() }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index e2124ec9f3..09cb471f10 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,4 +1,4 @@ ---- +# synced from @nextcloud/android-config name: 'Close stale issues' on: schedule: @@ -14,7 +14,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@v6 + - uses: actions/stale@5ebf00ea0e4c1561e9b43a292ed34424fb1d4578 # v6 with: days-before-stale: 28 days-before-close: 14 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index a64c5f69ef..f3e3baf73e 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -14,29 +14,28 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@1df8dbefe2a8cbc99770194893dd902763bee34b # v3 with: distribution: "temurin" java-version: 11 - name: Delete old comments env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} if: ${{ always() }} run: scripts/deleteOldComments.sh "test" "Unit" ${{github.event.number}} - name: Run unit tests with coverage - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # v2 with: arguments: jacocoTestGplayDebugUnitTest - name: Upload failing results if: ${{ failure() }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: - scripts/uploadReport.sh "${{ secrets.LOG_USERNAME }}" "${{ secrets.LOG_PASSWORD }}" ${{github.event.number}} "test" "Unit" ${{github.event.number}} + run: scripts/uploadReport.sh "${{ secrets.LOG_USERNAME }}" "${{ secrets.LOG_PASSWORD }}" ${{github.event.number}} "test" "Unit" ${{github.event.number}} - name: Upload coverage to codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3 with: token: ${{ secrets.CODECOV_TOKEN }} flags: unit diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open.png b/app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open.png index 6160fc4005..c70bcfaee7 100644 Binary files a/app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open.png and b/app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open.png differ diff --git a/app/src/main/java/com/nextcloud/client/database/DatabaseModule.kt b/app/src/main/java/com/nextcloud/client/database/DatabaseModule.kt index 822c0380c9..92aff3f3f2 100644 --- a/app/src/main/java/com/nextcloud/client/database/DatabaseModule.kt +++ b/app/src/main/java/com/nextcloud/client/database/DatabaseModule.kt @@ -25,6 +25,7 @@ package com.nextcloud.client.database import android.content.Context import com.nextcloud.client.core.Clock import com.nextcloud.client.database.dao.ArbitraryDataDao +import com.nextcloud.client.database.dao.FileDao import dagger.Module import dagger.Provides import javax.inject.Singleton @@ -42,4 +43,9 @@ class DatabaseModule { fun arbitraryDataDao(nextcloudDatabase: NextcloudDatabase): ArbitraryDataDao { return nextcloudDatabase.arbitraryDataDao() } + + @Provides + fun fileDao(nextcloudDatabase: NextcloudDatabase): FileDao { + return nextcloudDatabase.fileDao() + } } diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index 62b7045a59..64b19a55a8 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -30,6 +30,7 @@ import androidx.room.RoomDatabase import com.nextcloud.client.core.Clock import com.nextcloud.client.core.ClockImpl import com.nextcloud.client.database.dao.ArbitraryDataDao +import com.nextcloud.client.database.dao.FileDao import com.nextcloud.client.database.entity.ArbitraryDataEntity import com.nextcloud.client.database.entity.CapabilityEntity import com.nextcloud.client.database.entity.ExternalLinkEntity @@ -65,6 +66,7 @@ import com.owncloud.android.db.ProviderMeta abstract class NextcloudDatabase : RoomDatabase() { abstract fun arbitraryDataDao(): ArbitraryDataDao + abstract fun fileDao(): FileDao companion object { const val FIRST_ROOM_DB_VERSION = 65 diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt new file mode 100644 index 0000000000..21a7e34d70 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -0,0 +1,64 @@ +/* + * Nextcloud Android client application + * + * @author Dariusz Olszewski + * Copyright (C) 2022 Dariusz Olszewski + * Copyright (C) 2022 Nextcloud GmbH + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this program. If not, see . + * + */ + +package com.nextcloud.client.database.dao + +import androidx.room.Dao +import androidx.room.Query +import com.nextcloud.client.database.entity.FileEntity +import com.owncloud.android.db.ProviderMeta.ProviderTableMeta + +@Dao +interface FileDao { + @Query("SELECT * FROM filelist WHERE _id = :id LIMIT 1") + fun getFileById(id: Long): FileEntity? + + @Query("SELECT * FROM filelist WHERE path = :path AND file_owner = :fileOwner LIMIT 1") + fun getFileByEncryptedRemotePath(path: String, fileOwner: String): FileEntity? + + @Query("SELECT * FROM filelist WHERE path_decrypted = :path AND file_owner = :fileOwner LIMIT 1") + fun getFileByDecryptedRemotePath(path: String, fileOwner: String): FileEntity? + + @Query("SELECT * FROM filelist WHERE media_path = :path AND file_owner = :fileOwner LIMIT 1") + fun getFileByLocalPath(path: String, fileOwner: String): FileEntity? + + @Query("SELECT * FROM filelist WHERE remote_id = :remoteId AND file_owner = :fileOwner LIMIT 1") + fun getFileByRemoteId(remoteId: String, fileOwner: String): FileEntity? + + @Query("SELECT * FROM filelist WHERE parent = :parentId ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}") + fun getFolderContent(parentId: Long): List + + @Query( + "SELECT * FROM filelist WHERE modified >= :startDate" + + " AND modified < :endDate" + + " AND (content_type LIKE 'image/%' OR content_type LIKE 'video/%')" + + " AND file_owner = :fileOwner" + + " ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}" + ) + fun getGalleryItems(startDate: Long, endDate: Long, fileOwner: String): List + + @Query("SELECT * FROM filelist WHERE file_owner = :fileOwner ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}") + fun getAllFiles(fileOwner: String): List + + @Query("SELECT * FROM filelist WHERE path LIKE :pathPattern AND file_owner = :fileOwner ORDER BY path ASC") + fun getFolderWithDescendants(pathPattern: String, fileOwner: String): List +} diff --git a/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt index e05bd6fd63..f02a06af2a 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt @@ -31,7 +31,7 @@ import com.owncloud.android.db.ProviderMeta.ProviderTableMeta data class FileEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = ProviderTableMeta._ID) - val id: Int?, + val id: Long?, @ColumnInfo(name = ProviderTableMeta.FILE_NAME) val name: String?, @ColumnInfo(name = ProviderTableMeta.FILE_ENCRYPTED_NAME) @@ -41,25 +41,25 @@ data class FileEntity( @ColumnInfo(name = ProviderTableMeta.FILE_PATH_DECRYPTED) val pathDecrypted: String?, @ColumnInfo(name = ProviderTableMeta.FILE_PARENT) - val parent: Int?, + val parent: Long?, @ColumnInfo(name = ProviderTableMeta.FILE_CREATION) - val creation: Int?, + val creation: Long?, @ColumnInfo(name = ProviderTableMeta.FILE_MODIFIED) - val modified: Int?, + val modified: Long?, @ColumnInfo(name = ProviderTableMeta.FILE_CONTENT_TYPE) val contentType: String?, @ColumnInfo(name = ProviderTableMeta.FILE_CONTENT_LENGTH) - val contentLength: Int?, + val contentLength: Long?, @ColumnInfo(name = ProviderTableMeta.FILE_STORAGE_PATH) val storagePath: String?, @ColumnInfo(name = ProviderTableMeta.FILE_ACCOUNT_OWNER) val accountOwner: String?, @ColumnInfo(name = ProviderTableMeta.FILE_LAST_SYNC_DATE) - val lastSyncDate: Int?, + val lastSyncDate: Long?, @ColumnInfo(name = ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA) - val lastSyncDateForData: Int?, + val lastSyncDateForData: Long?, @ColumnInfo(name = ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA) - val modifiedAtLastSyncForData: Int?, + val modifiedAtLastSyncForData: Long?, @ColumnInfo(name = ProviderTableMeta.FILE_ETAG) val etag: String?, @ColumnInfo(name = ProviderTableMeta.FILE_ETAG_ON_SERVER) @@ -111,7 +111,7 @@ data class FileEntity( @ColumnInfo(name = ProviderTableMeta.FILE_LOCK_OWNER_EDITOR) val lockOwnerEditor: String?, @ColumnInfo(name = ProviderTableMeta.FILE_LOCK_TIMESTAMP) - val lockTimestamp: Int?, + val lockTimestamp: Long?, @ColumnInfo(name = ProviderTableMeta.FILE_LOCK_TIMEOUT) val lockTimeout: Int?, @ColumnInfo(name = ProviderTableMeta.FILE_LOCK_TOKEN) diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index b3eab7a91c..4321696775 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -41,6 +41,9 @@ import android.text.TextUtils; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import com.nextcloud.client.account.User; +import com.nextcloud.client.database.NextcloudDatabase; +import com.nextcloud.client.database.dao.FileDao; +import com.nextcloud.client.database.entity.FileEntity; import com.owncloud.android.MainApp; import com.owncloud.android.db.ProviderMeta.ProviderTableMeta; import com.owncloud.android.lib.common.network.WebdavEntry; @@ -91,6 +94,9 @@ public class FileDataStorageManager { private final ContentProviderClient contentProviderClient; private final User user; + private final FileDao fileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).fileDao(); + private final Gson gson = new Gson(); + public FileDataStorageManager(User user, ContentResolver contentResolver) { this.contentProviderClient = null; this.contentResolver = contentResolver; @@ -122,65 +128,53 @@ public class FileDataStorageManager { private @Nullable OCFile getFileByPath(String type, String path) { - Cursor cursor = getFileCursorForValue(type, path); - OCFile ocFile = null; - if (cursor.moveToFirst()) { - ocFile = createFileInstance(cursor); - } - cursor.close(); + final boolean shouldUseEncryptedPath = ProviderTableMeta.FILE_PATH.equals(type); + FileEntity fileEntity = shouldUseEncryptedPath ? + fileDao.getFileByEncryptedRemotePath(path, user.getAccountName()) : + fileDao.getFileByDecryptedRemotePath(path, user.getAccountName()); - if (ocFile == null && OCFile.ROOT_PATH.equals(path)) { + if (fileEntity != null) { + return createFileInstance(fileEntity); + } + + if (OCFile.ROOT_PATH.equals(path)) { return createRootDir(); // root should always exist } - return ocFile; + return null; } public @Nullable OCFile getFileById(long id) { - Cursor cursor = getFileCursorForValue(ProviderTableMeta._ID, String.valueOf(id)); - OCFile ocFile = null; - - if (cursor.moveToFirst()) { - ocFile = createFileInstance(cursor); + FileEntity fileEntity = fileDao.getFileById(id); + if (fileEntity != null) { + return createFileInstance(fileEntity); } - cursor.close(); - - return ocFile; + return null; } public @Nullable OCFile getFileByLocalPath(String path) { - Cursor cursor = getFileCursorForValue(ProviderTableMeta.FILE_STORAGE_PATH, path); - OCFile ocFile = null; - - if (cursor.moveToFirst()) { - ocFile = createFileInstance(cursor); + FileEntity fileEntity = fileDao.getFileByLocalPath(path, user.getAccountName()); + if (fileEntity != null) { + return createFileInstance(fileEntity); } - cursor.close(); - - return ocFile; + return null; } public @Nullable OCFile getFileByRemoteId(String remoteId) { - Cursor cursor = getFileCursorForValue(ProviderTableMeta.FILE_REMOTE_ID, remoteId); - OCFile ocFile = null; - - if (cursor.moveToFirst()) { - ocFile = createFileInstance(cursor); + FileEntity fileEntity = fileDao.getFileByRemoteId(remoteId, user.getAccountName()); + if (fileEntity != null) { + return createFileInstance(fileEntity); } - cursor.close(); - - return ocFile; + return null; } - public boolean fileExists(long id) { - return fileExists(ProviderTableMeta._ID, String.valueOf(id)); - } + public boolean fileExists(long id) { return fileDao.getFileById(id) != null; } public boolean fileExists(String path) { - return fileExists(ProviderTableMeta.FILE_PATH, path); + return fileDao.getFileByEncryptedRemotePath(path, user.getAccountName()) != null; } @@ -662,82 +656,60 @@ public class FileDataStorageManager { throw new IllegalStateException("Parent folder of the target path does not exist!!"); } - /// 1. get all the descendants of the moved element in a single QUERY - Cursor cursor = null; - if (getContentProviderClient() != null) { - try { - cursor = getContentProviderClient().query( - ProviderTableMeta.CONTENT_URI, - null, - ProviderTableMeta.FILE_ACCOUNT_OWNER + AND + ProviderTableMeta.FILE_PATH + " LIKE ? ", - new String[]{user.getAccountName(), ocFile.getRemotePath() + "%"}, - ProviderTableMeta.FILE_PATH + " ASC " - ); - } catch (RemoteException e) { - Log_OC.e(TAG, e.getMessage(), e); - } + String oldPath = ocFile.getRemotePath(); - } else { - cursor = getContentResolver().query( - ProviderTableMeta.CONTENT_URI, - null, - ProviderTableMeta.FILE_ACCOUNT_OWNER + AND + ProviderTableMeta.FILE_PATH + " LIKE ? ", - new String[]{user.getAccountName(), ocFile.getRemotePath() + "%"}, - ProviderTableMeta.FILE_PATH + " ASC " - ); - } + /// 1. get all the descendants of the moved element in a single QUERY + List fileEntities = + fileDao.getFolderWithDescendants(oldPath + "%", user.getAccountName()); /// 2. prepare a batch of update operations to change all the descendants - ArrayList operations = new ArrayList<>(cursor.getCount()); + ArrayList operations = new ArrayList<>(fileEntities.size()); String defaultSavePath = FileStorageUtils.getSavePath(user.getAccountName()); List originalPathsToTriggerMediaScan = new ArrayList<>(); List newPathsToTriggerMediaScan = new ArrayList<>(); - if (cursor.moveToFirst()) { - int lengthOfOldPath = ocFile.getRemotePath().length(); - int lengthOfOldStoragePath = defaultSavePath.length() + lengthOfOldPath; - do { - ContentValues contentValues = new ContentValues(); // keep construction in the loop - OCFile childFile = createFileInstance(cursor); + int lengthOfOldPath = oldPath.length(); + int lengthOfOldStoragePath = defaultSavePath.length() + lengthOfOldPath; + for (FileEntity fileEntity: fileEntities) { + ContentValues contentValues = new ContentValues(); // keep construction in the loop + OCFile childFile = createFileInstance(fileEntity); + contentValues.put( + ProviderTableMeta.FILE_PATH, + targetPath + childFile.getRemotePath().substring(lengthOfOldPath) + ); + + if (!childFile.isEncrypted()) { contentValues.put( - ProviderTableMeta.FILE_PATH, + ProviderTableMeta.FILE_PATH_DECRYPTED, targetPath + childFile.getRemotePath().substring(lengthOfOldPath) ); + } - if (!childFile.isEncrypted()) { - contentValues.put( - ProviderTableMeta.FILE_PATH_DECRYPTED, - targetPath + childFile.getRemotePath().substring(lengthOfOldPath) - ); + if (childFile.getStoragePath() != null && childFile.getStoragePath().startsWith(defaultSavePath)) { + // update link to downloaded content - but local move is not done here! + String targetLocalPath = defaultSavePath + targetPath + + childFile.getStoragePath().substring(lengthOfOldStoragePath); + + contentValues.put(ProviderTableMeta.FILE_STORAGE_PATH, targetLocalPath); + + if (MimeTypeUtil.isMedia(childFile.getMimeType())) { + originalPathsToTriggerMediaScan.add(childFile.getStoragePath()); + newPathsToTriggerMediaScan.add(targetLocalPath); } - if (childFile.getStoragePath() != null && childFile.getStoragePath().startsWith(defaultSavePath)) { - // update link to downloaded content - but local move is not done here! - String targetLocalPath = defaultSavePath + targetPath + - childFile.getStoragePath().substring(lengthOfOldStoragePath); + } - contentValues.put(ProviderTableMeta.FILE_STORAGE_PATH, targetLocalPath); + if (childFile.getRemotePath().equals(ocFile.getRemotePath())) { + contentValues.put(ProviderTableMeta.FILE_PARENT, targetParent.getFileId()); + } - if (MimeTypeUtil.isMedia(childFile.getMimeType())) { - originalPathsToTriggerMediaScan.add(childFile.getStoragePath()); - newPathsToTriggerMediaScan.add(targetLocalPath); - } + operations.add( + ContentProviderOperation.newUpdate(ProviderTableMeta.CONTENT_URI) + .withValues(contentValues) + .withSelection(ProviderTableMeta._ID + " = ?", new String[]{String.valueOf(childFile.getFileId())}) + .build()); - } - - if (childFile.getRemotePath().equals(ocFile.getRemotePath())) { - contentValues.put(ProviderTableMeta.FILE_PARENT, targetParent.getFileId()); - } - - operations.add( - ContentProviderOperation.newUpdate(ProviderTableMeta.CONTENT_URI) - .withValues(contentValues) - .withSelection(ProviderTableMeta._ID + " = ?", new String[]{String.valueOf(childFile.getFileId())}) - .build()); - - } while (cursor.moveToNext()); } - cursor.close(); /// 3. apply updates in batch try { @@ -861,46 +833,18 @@ public class FileDataStorageManager { } private List getFolderContent(long parentId, boolean onlyOnDevice) { + Log_OC.d(TAG, "getFolderContent - start"); List folderContent = new ArrayList<>(); - Uri requestURI = Uri.withAppendedPath(ProviderTableMeta.CONTENT_URI_DIR, String.valueOf(parentId)); - Cursor cursor; - - if (getContentProviderClient() != null) { - try { - cursor = getContentProviderClient().query( - requestURI, - null, - ProviderTableMeta.FILE_PARENT + "=?", - new String[]{String.valueOf(parentId)}, - null - ); - } catch (RemoteException e) { - Log_OC.e(TAG, e.getMessage(), e); - return folderContent; + List files = fileDao.getFolderContent(parentId); + for (FileEntity fileEntity: files) { + OCFile child = createFileInstance(fileEntity); + if (!onlyOnDevice || child.existsOnDevice()) { + folderContent.add(child); } - } else { - cursor = getContentResolver().query( - requestURI, - null, - ProviderTableMeta.FILE_PARENT + "=?", - new String[]{String.valueOf(parentId)}, - null - ); - } - - if (cursor != null) { - if (cursor.moveToFirst()) { - do { - OCFile child = createFileInstance(cursor); - if (!onlyOnDevice || child.existsOnDevice()) { - folderContent.add(child); - } - } while (cursor.moveToNext()); - } - cursor.close(); } + Log_OC.d(TAG, "getFolderContent - finished"); return folderContent; } @@ -914,47 +858,6 @@ public class FileDataStorageManager { return ocFile; } - // TODO write test - private boolean fileExists(String key, String value) { - Cursor cursor = getFileCursorForValue(key, value); - boolean isExists = false; - - if (cursor == null) { - Log_OC.e(TAG, "Couldn't determine file existance, assuming non existance"); - } else { - isExists = cursor.moveToFirst(); - cursor.close(); - } - - return isExists; - } - - private Cursor getFileCursorForValue(String key, String value) { - Cursor cursor; - if (getContentResolver() != null) { - cursor = getContentResolver() - .query(ProviderTableMeta.CONTENT_URI, - null, - key + AND - + ProviderTableMeta.FILE_ACCOUNT_OWNER - + "=?", - new String[]{value, user.getAccountName()}, null); - } else { - try { - cursor = getContentProviderClient().query( - ProviderTableMeta.CONTENT_URI, - null, - key + AND + ProviderTableMeta.FILE_ACCOUNT_OWNER - + "=?", new String[]{value, user.getAccountName()}, - null); - } catch (RemoteException e) { - Log_OC.e(TAG, "Could not get file details: " + e.getMessage(), e); - cursor = null; - } - } - return cursor; - } - @Nullable private OCFile createFileInstanceFromVirtual(Cursor cursor) { long fileId = cursor.getLong(cursor.getColumnIndexOrThrow(ProviderTableMeta.VIRTUAL_OCFILE_ID)); @@ -962,85 +865,88 @@ public class FileDataStorageManager { return getFileById(fileId); } - private OCFile createFileInstance(Cursor cursor) { - OCFile ocFile = null; - if (cursor != null) { - ocFile = new OCFile(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_PATH))); - ocFile.setDecryptedRemotePath(getString(cursor, ProviderTableMeta.FILE_PATH_DECRYPTED)); - ocFile.setFileId(cursor.getLong(cursor.getColumnIndexOrThrow(ProviderTableMeta._ID))); - ocFile.setParentId(cursor.getLong(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_PARENT))); - ocFile.setMimeType(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_CONTENT_TYPE))); - ocFile.setStoragePath(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_STORAGE_PATH))); - if (ocFile.getStoragePath() == null) { - // try to find existing file and bind it with current account; - // with the current update of SynchronizeFolderOperation, this won't be - // necessary anymore after a full synchronization of the account - File file = new File(FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), ocFile)); - if (file.exists()) { - ocFile.setStoragePath(file.getAbsolutePath()); - ocFile.setLastSyncDateForData(file.lastModified()); - } + private int nullToZero(Integer i) { + return (i == null) ? 0 : i; + } + + private long nullToZero(Long i) { + return (i == null) ? 0 : i; + } + + private OCFile createFileInstance(FileEntity fileEntity) { + OCFile ocFile = new OCFile(fileEntity.getPath()); + ocFile.setDecryptedRemotePath(fileEntity.getPathDecrypted()); + ocFile.setFileId(nullToZero(fileEntity.getId())); + ocFile.setParentId(nullToZero(fileEntity.getParent())); + ocFile.setMimeType(fileEntity.getContentType()); + ocFile.setStoragePath(fileEntity.getStoragePath()); + if (ocFile.getStoragePath() == null) { + // try to find existing file and bind it with current account; + // with the current update of SynchronizeFolderOperation, this won't be + // necessary anymore after a full synchronization of the account + File file = new File(FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), ocFile)); + if (file.exists()) { + ocFile.setStoragePath(file.getAbsolutePath()); + ocFile.setLastSyncDateForData(file.lastModified()); } - ocFile.setFileLength(cursor.getLong(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_CONTENT_LENGTH))); - ocFile.setCreationTimestamp(cursor.getLong(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_CREATION))); - ocFile.setModificationTimestamp(cursor.getLong(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_MODIFIED))); - ocFile.setModificationTimestampAtLastSyncForData(cursor.getLong( - cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA))); - ocFile.setLastSyncDateForProperties(cursor.getLong(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_LAST_SYNC_DATE))); - ocFile.setLastSyncDateForData(cursor.getLong(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA))); - ocFile.setEtag(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_ETAG))); - ocFile.setEtagOnServer(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_ETAG_ON_SERVER))); - ocFile.setSharedViaLink(cursor.getInt(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_SHARED_VIA_LINK)) == 1); - ocFile.setSharedWithSharee(cursor.getInt(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_SHARED_WITH_SHAREE)) == 1); - ocFile.setPermissions(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_PERMISSIONS))); - ocFile.setRemoteId(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_REMOTE_ID))); - ocFile.setUpdateThumbnailNeeded(cursor.getInt(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_UPDATE_THUMBNAIL)) == 1); - ocFile.setDownloading(cursor.getInt(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_IS_DOWNLOADING)) == 1); - ocFile.setEtagInConflict(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_ETAG_IN_CONFLICT))); - ocFile.setFavorite(cursor.getInt(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_FAVORITE)) == 1); - ocFile.setEncrypted(cursor.getInt(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_IS_ENCRYPTED)) == 1); -// if (ocFile.isEncrypted()) { -// ocFile.setFileName(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_NAME))); -// } - ocFile.setMountType(WebdavEntry.MountType.values()[cursor.getInt( - cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_MOUNT_TYPE))]); - ocFile.setPreviewAvailable(cursor.getInt(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_HAS_PREVIEW)) == 1); - ocFile.setUnreadCommentsCount(cursor.getInt(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_UNREAD_COMMENTS_COUNT))); - ocFile.setOwnerId(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_OWNER_ID))); - ocFile.setOwnerDisplayName(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_OWNER_DISPLAY_NAME))); - ocFile.setNote(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_NOTE))); - ocFile.setRichWorkspace(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_RICH_WORKSPACE))); - ocFile.setLocked(cursor.getInt(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_LOCKED)) == 1); - final int lockTypeInt = cursor.getInt(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_LOCK_TYPE)); - ocFile.setLockType(lockTypeInt != -1 ? FileLockType.fromValue(lockTypeInt) : null); - ocFile.setLockOwnerId(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_LOCK_OWNER))); - ocFile.setLockOwnerDisplayName(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_LOCK_OWNER_DISPLAY_NAME))); - ocFile.setLockOwnerEditor(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_LOCK_OWNER_EDITOR))); - ocFile.setLockTimestamp(cursor.getInt(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_LOCK_TIMESTAMP))); - ocFile.setLockTimeout(cursor.getInt(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_LOCK_TIMEOUT))); - ocFile.setLockToken(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_LOCK_TOKEN))); + } + ocFile.setFileLength(nullToZero(fileEntity.getContentLength())); + ocFile.setCreationTimestamp(nullToZero(fileEntity.getCreation())); + ocFile.setModificationTimestamp(nullToZero(fileEntity.getModified())); + ocFile.setModificationTimestampAtLastSyncForData(nullToZero(fileEntity.getModifiedAtLastSyncForData())); + ocFile.setLastSyncDateForProperties(nullToZero(fileEntity.getLastSyncDate())); + ocFile.setLastSyncDateForData(nullToZero(fileEntity.getLastSyncDateForData())); + ocFile.setEtag(fileEntity.getEtag()); + ocFile.setEtagOnServer(fileEntity.getEtagOnServer()); + ocFile.setSharedViaLink(nullToZero(fileEntity.getSharedViaLink()) == 1); + ocFile.setSharedWithSharee(nullToZero(fileEntity.getSharedWithSharee()) == 1); + ocFile.setPermissions(fileEntity.getPermissions()); + ocFile.setRemoteId(fileEntity.getRemoteId()); + ocFile.setUpdateThumbnailNeeded(nullToZero(fileEntity.getUpdateThumbnail()) == 1); + ocFile.setDownloading(nullToZero(fileEntity.isDownloading()) == 1); + ocFile.setEtagInConflict(fileEntity.getEtagInConflict()); + ocFile.setFavorite(nullToZero(fileEntity.getFavorite()) == 1); + ocFile.setEncrypted(nullToZero(fileEntity.isEncrypted()) == 1); +// if (ocFile.isEncrypted()) { +// ocFile.setFileName(cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_NAME))); +// } + Integer mountType = fileEntity.getMountType(); // TODO - any default when NULL returned? + if (mountType != null) { + ocFile.setMountType(WebdavEntry.MountType.values()[mountType]); + } + ocFile.setPreviewAvailable(nullToZero(fileEntity.getHasPreview()) == 1); + ocFile.setUnreadCommentsCount(nullToZero(fileEntity.getUnreadCommentsCount())); + ocFile.setOwnerId(fileEntity.getOwnerId()); + ocFile.setOwnerDisplayName(fileEntity.getOwnerDisplayName()); + ocFile.setNote(fileEntity.getNote()); + ocFile.setRichWorkspace(fileEntity.getRichWorkspace()); + ocFile.setLocked(nullToZero(fileEntity.getLocked()) == 1); + final int lockTypeInt = nullToZero(fileEntity.getLockType()); // TODO - what value should be used for NULL??? + ocFile.setLockType(lockTypeInt != -1 ? FileLockType.fromValue(lockTypeInt) : null); + ocFile.setLockOwnerId(fileEntity.getLockOwner()); + ocFile.setLockOwnerDisplayName(fileEntity.getLockOwnerDisplayName()); + ocFile.setLockOwnerEditor(fileEntity.getLockOwnerEditor()); + ocFile.setLockTimestamp(nullToZero(fileEntity.getLockTimestamp())); + ocFile.setLockTimeout(nullToZero(fileEntity.getLockTimeout())); + ocFile.setLockToken(fileEntity.getLockToken()); - - String sharees = cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_SHAREES)); - - if (sharees == null || NULL_STRING.equals(sharees) || sharees.isEmpty()) { + String sharees = fileEntity.getSharees(); + if (sharees == null || NULL_STRING.equals(sharees) || sharees.isEmpty()) { + ocFile.setSharees(new ArrayList<>()); + } else { + try { + ShareeUser[] shareesArray = gson.fromJson(sharees, ShareeUser[].class); + ocFile.setSharees(new ArrayList<>(Arrays.asList(shareesArray))); + } catch (JsonSyntaxException e) { + // ignore saved value due to api change ocFile.setSharees(new ArrayList<>()); - } else { - try { - ShareeUser[] shareesArray = new Gson().fromJson(sharees, ShareeUser[].class); - - ocFile.setSharees(new ArrayList<>(Arrays.asList(shareesArray))); - } catch (JsonSyntaxException e) { - // ignore saved value due to api change - ocFile.setSharees(new ArrayList<>()); - } } - String metadataSize = cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_METADATA_SIZE)); - ImageDimension imageDimension = new Gson().fromJson(metadataSize, ImageDimension.class); + } - if (imageDimension != null) { - ocFile.setImageDimension(imageDimension); - } + String metadataSize = fileEntity.getMetadataSize(); + ImageDimension imageDimension = gson.fromJson(metadataSize, ImageDimension.class); + if (imageDimension != null) { + ocFile.setImageDimension(imageDimension); } return ocFile; @@ -2193,64 +2099,17 @@ public class FileDataStorageManager { } public List getGalleryItems(long startDate, long endDate) { - List files = new ArrayList<>(); + Log_OC.d(TAG, "getGalleryItems - start: " + startDate + ", " + endDate); - Uri requestURI = ProviderTableMeta.CONTENT_URI; - Cursor cursor; + List fileEntities = fileDao.getGalleryItems(startDate, endDate, user.getAccountName()); + Log_OC.d(TAG, "getGalleryItems - query complete, list size: " + fileEntities.size()); - if (getContentProviderClient() != null) { - try { - cursor = getContentProviderClient().query( - requestURI, - null, - ProviderTableMeta.FILE_ACCOUNT_OWNER + AND + - ProviderTableMeta.FILE_MODIFIED + ">=? AND " + - ProviderTableMeta.FILE_MODIFIED + "=? AND " + - ProviderTableMeta.FILE_MODIFIED + " files = new ArrayList<>(fileEntities.size()); + for (FileEntity fileEntity: fileEntities) { + files.add(createFileInstance(fileEntity)); } + Log_OC.d(TAG, "getGalleryItems - finished"); return files; } @@ -2338,32 +2197,12 @@ public class FileDataStorageManager { } public List getAllFiles() { - String selection = ProviderTableMeta.FILE_ACCOUNT_OWNER + "= ? "; - String[] selectionArgs = new String[]{user.getAccountName()}; + // TODO - Apparently this method is used only by tests + List fileEntities = fileDao.getAllFiles(user.getAccountName()); + List folderContent = new ArrayList<>(fileEntities.size()); - List folderContent = new ArrayList<>(); - - Uri requestURI = ProviderTableMeta.CONTENT_URI_DIR; - Cursor cursor; - - if (getContentProviderClient() != null) { - try { - cursor = getContentProviderClient().query(requestURI, null, selection, selectionArgs, null); - } catch (RemoteException e) { - Log_OC.e(TAG, e.getMessage(), e); - return folderContent; - } - } else { - cursor = getContentResolver().query(requestURI, null, selection, selectionArgs, null); - } - - if (cursor != null) { - if (cursor.moveToFirst()) { - do { - folderContent.add(createFileInstance(cursor)); - } while (cursor.moveToNext()); - } - cursor.close(); + for (FileEntity fileEntity: fileEntities) { + folderContent.add(createFileInstance(fileEntity)); } return folderContent; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt index 56f2e9458e..b9b7f11ef3 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt @@ -53,6 +53,7 @@ import com.owncloud.android.utils.DataHolderUtil import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.ErrorMessageAdapter import com.owncloud.android.utils.FileSortOrder +import com.owncloud.android.utils.PathUtils import java.io.File import javax.inject.Inject @@ -74,6 +75,9 @@ open class FolderPickerActivity : private var mChooseBtn: MaterialButton? = null private var caption: String? = null + private var mAction: String? = null + private var mTargetFilePaths: ArrayList? = null + @Inject lateinit var localBroadcastManager: LocalBroadcastManager @@ -95,8 +99,9 @@ open class FolderPickerActivity : View.VISIBLE findViewById(R.id.switch_grid_view_button).visibility = View.GONE - if (intent.getStringExtra(EXTRA_ACTION) != null) { - when (intent.getStringExtra(EXTRA_ACTION)) { + mAction = intent.getStringExtra(EXTRA_ACTION) + if (mAction != null) { + when (mAction) { MOVE -> { caption = resources.getText(R.string.move_to).toString() mSearchOnlyFolders = true @@ -118,9 +123,8 @@ open class FolderPickerActivity : } else { caption = themeUtils.getDefaultDisplayNameForRootFolder(this) } - if (intent.getParcelableExtra(EXTRA_CURRENT_FOLDER) != null) { - file = intent.getParcelableExtra(EXTRA_CURRENT_FOLDER) - } + mTargetFilePaths = intent.getStringArrayListExtra(EXTRA_FILE_PATHS) + if (savedInstanceState == null) { createFragments() } @@ -146,7 +150,7 @@ open class FolderPickerActivity : val listOfFolders = listOfFilesFragment listOfFolders!!.listDirectory(folder, false, false) startSyncFolderOperation(folder, false) - updateNavigationElementsInActionBar() + updateUiElements() } } @@ -205,7 +209,7 @@ open class FolderPickerActivity : */ override fun onBrowsedDownTo(directory: OCFile) { file = directory - updateNavigationElementsInActionBar() + updateUiElements() // Sync Folder startSyncFolderOperation(directory, false) } @@ -241,6 +245,9 @@ open class FolderPickerActivity : // refresh list of files refreshListOfFilesFragment(false) + file = listOfFilesFragment?.currentFile + updateUiElements() + // Listen for sync messages val syncIntentFilter = IntentFilter(FileSyncAdapter.EVENT_FULL_SYNC_START) syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_END) @@ -317,7 +324,7 @@ open class FolderPickerActivity : val root = storageManager.getFileByPath(OCFile.ROOT_PATH) listOfFiles.listDirectory(root, false, false) file = listOfFiles.currentFile - updateNavigationElementsInActionBar() + updateUiElements() startSyncFolderOperation(root, false) } } @@ -331,7 +338,30 @@ open class FolderPickerActivity : return } file = listOfFiles.currentFile - updateNavigationElementsInActionBar() + updateUiElements() + } + } + + private fun updateUiElements() { + toggleChooseEnabled() + updateNavigationElementsInActionBar() + } + + private fun toggleChooseEnabled() { + mChooseBtn?.isEnabled = checkFolderSelectable() + } + + // for copy and move, disable selecting parent folder of target files + private fun checkFolderSelectable(): Boolean { + return when { + mAction != COPY && mAction != MOVE -> true + mTargetFilePaths.isNullOrEmpty() -> true + file?.isFolder != true -> true + // all of the target files are already in the selected directory + mTargetFilePaths!!.all { PathUtils.isDirectParent(file.remotePath, it) } -> false + // some of the target files are parents of the selected folder + mTargetFilePaths!!.any { PathUtils.isAncestor(it, file.remotePath) } -> false + else -> true } } @@ -378,9 +408,8 @@ open class FolderPickerActivity : if (targetFiles != null) { resultData.putParcelableArrayListExtra(EXTRA_FILES, targetFiles) } - val targetFilePaths = i.getStringArrayListExtra(EXTRA_FILE_PATHS) - if (targetFilePaths != null) { - resultData.putStringArrayListExtra(EXTRA_FILE_PATHS, targetFilePaths) + mTargetFilePaths.let { + resultData.putStringArrayListExtra(EXTRA_FILE_PATHS, it) } setResult(RESULT_OK, resultData) finish() @@ -560,9 +589,6 @@ open class FolderPickerActivity : @JvmField val EXTRA_ACTION = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_ACTION") - @JvmField - val EXTRA_CURRENT_FOLDER = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_CURRENT_FOLDER") - const val MOVE = "MOVE" const val COPY = "COPY" const val CHOOSE_LOCATION = "CHOOSE_LOCATION" diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index 438027b82d..42d3a4d450 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -1227,7 +1227,6 @@ public class OCFileListFragment extends ExtendedListFragment implements paths.add(file.getRemotePath()); } action.putStringArrayListExtra(FolderPickerActivity.EXTRA_FILE_PATHS, paths); - action.putExtra(FolderPickerActivity.EXTRA_CURRENT_FOLDER, mFile); action.putExtra(FolderPickerActivity.EXTRA_ACTION, extraAction); getActivity().startActivityForResult(action, requestCode); } diff --git a/app/src/main/java/com/owncloud/android/utils/PathUtils.kt b/app/src/main/java/com/owncloud/android/utils/PathUtils.kt new file mode 100644 index 0000000000..342d893abb --- /dev/null +++ b/app/src/main/java/com/owncloud/android/utils/PathUtils.kt @@ -0,0 +1,49 @@ +/* + * Nextcloud Android client application + * + * @author Álvaro Brey + * Copyright (C) 2022 Álvaro Brey + * Copyright (C) 2022 Nextcloud GmbH + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this program. If not, see . + * + */ + +package com.owncloud.android.utils + +import com.owncloud.android.datamodel.OCFile +import java.io.File + +object PathUtils { + /** + * Returns `true` if [folderPath] is a direct parent of [filePath], `false` otherwise + */ + fun isDirectParent(folderPath: String, filePath: String): Boolean { + return File(folderPath).path == File(filePath).parent + } + + /** + * Returns `true` if [folderPath] is an ancestor of [filePath], `false` otherwise + * + * If [isDirectParent] is `true` for the same arguments, this function should return `true` as well + */ + fun isAncestor(folderPath: String, filePath: String): Boolean { + if (folderPath.isEmpty() || filePath.isEmpty()) { + return false + } + val folderPathWithSlash = + if (folderPath.endsWith(OCFile.PATH_SEPARATOR)) folderPath else folderPath + OCFile.PATH_SEPARATOR + return filePath.startsWith(folderPathWithSlash) + } +} diff --git a/app/src/main/res/layout/community_layout.xml b/app/src/main/res/layout/community_layout.xml index 880adb62aa..9e1ab9bcda 100755 --- a/app/src/main/res/layout/community_layout.xml +++ b/app/src/main/res/layout/community_layout.xml @@ -69,15 +69,6 @@ android:theme="@style/Button.Primary" app:cornerRadius="@dimen/button_corner_radius" /> - - Fehler gefunden? Komisches Verhalten? Helfen Sie durch Testen Fehlerbericht auf GitHub erstellen - Möchten Sie uns beim Testen der nächsten Version unterstützen? + Helfen Sie uns beim Testen der nächsten Version! Konfigurieren Lokale Verschlüsselung entfernen Wollen Sie %1$s wirklich löschen? diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 840f042967..e4ad6665ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -523,7 +523,6 @@ Help by testing Found a bug? Oddments? Report an issue on GitHub - Interested in helping out by testing what will be the next version? Test the dev version This includes all upcoming features and it is on the very bleeding edge. Bugs/errors can occur, if and when they do, please report of your findings. Release candidate diff --git a/app/src/test/java/com/owncloud/android/utils/PathUtilsTest.kt b/app/src/test/java/com/owncloud/android/utils/PathUtilsTest.kt new file mode 100644 index 0000000000..f83eaadc42 --- /dev/null +++ b/app/src/test/java/com/owncloud/android/utils/PathUtilsTest.kt @@ -0,0 +1,114 @@ +/* + * Nextcloud Android client application + * + * @author Álvaro Brey + * Copyright (C) 2022 Álvaro Brey + * Copyright (C) 2022 Nextcloud GmbH + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this program. If not, see . + * + */ + +package com.owncloud.android.utils + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Suite + +private val directParents: Array> = arrayOf( + arrayOf("/bar", "/bar/foo.tgz", true), + arrayOf("/bar/", "/bar/foo.tgz", true), + arrayOf("/bar/", "/bar/foo/", true), + arrayOf("/bar/", "/bar/foo", true), + arrayOf("/", "/bar/", true), + arrayOf("/bar/", "/foo/bar", false) +) + +private val nonAncestors: Array> = arrayOf( + arrayOf("/bar/", "/", false), + arrayOf("/bar/", "", false), + arrayOf("/", "", false), + arrayOf("", "", false), + arrayOf("", "/", false) +) + +/** + * These should return `false` for [PathUtils.isDirectParent] but `true` for [PathUtils.isAncestor] + */ +private val indirectAncestors: List> = listOf( + Pair("/bar", "/bar/foo/baz.tgz"), + Pair("/bar/", "/bar/foo/baz.tgz"), + Pair("/bar/", "/bar/foo/baz/"), + Pair("/bar/", "/bar/foo/baz") +) + +@RunWith(Suite::class) +@Suite.SuiteClasses( + PathUtilsTest.IsDirectParent::class, + PathUtilsTest.IsAncestor::class +) +class PathUtilsTest { + + @RunWith(Parameterized::class) + internal class IsDirectParent( + private val folderPath: String, + private val filePath: String, + private val isParent: Boolean + ) { + + @Test + fun testIsParent() { + assertEquals("Wrong isParentPath result", isParent, PathUtils.isDirectParent(folderPath, filePath)) + } + + companion object { + @Parameterized.Parameters(name = "{0}, {1} => {2}") + @JvmStatic + fun urls(): Array> { + val otherAncestors: Array> = indirectAncestors.map { + @Suppress("UNCHECKED_CAST") + arrayOf(it.first, it.second, false) as Array + }.toTypedArray() + return directParents + nonAncestors + otherAncestors + } + } + } + + @RunWith(Parameterized::class) + internal class IsAncestor( + private val folderPath: String, + private val filePath: String, + private val isAscendant: Boolean + ) { + + @Test + fun testIsAncestor() { + assertEquals("Wrong isParentPath result", isAscendant, PathUtils.isAncestor(folderPath, filePath)) + } + + companion object { + @Parameterized.Parameters(name = "{0}, {1} => {2}") + @JvmStatic + fun urls(): Array> { + val otherAncestors: Array> = indirectAncestors.map { + @Suppress("UNCHECKED_CAST") + arrayOf(it.first, it.second, true) as Array + }.toTypedArray() + return directParents + nonAncestors + otherAncestors + } + } + } +}