Merge remote-tracking branch 'origin/master' into dev

This commit is contained in:
Tobias Kaminsky 2022-12-17 03:34:14 +01:00
commit fc92c734e7
25 changed files with 519 additions and 432 deletions

View file

@ -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

View file

@ -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

View file

@ -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 }}"

View file

@ -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

View file

@ -26,17 +26,17 @@ jobs:
language: [ 'java' ]
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
- name: Set Swap Space
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c #v1.0
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0
with:
swap-size-gb: 10
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2
with:
languages: ${{ matrix.language }}
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@1df8dbefe2a8cbc99770194893dd902763bee34b # v3
with:
distribution: "temurin"
java-version: 11
@ -46,4 +46,4 @@ jobs:
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
uses: github/codeql-action/analyze@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2

View file

@ -1,3 +1,4 @@
# synced from @nextcloud/android-config
name: "Detect new java files"
on:
@ -11,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: file_changes
uses: trilom/file-changes-action@v1.2.4
uses: trilom/file-changes-action@a6ca26c14274c33b15e6499323aac178af06ad4b # v1.2.4
with:
output: ','
- name: Detect new java files
@ -29,4 +30,3 @@ jobs:
echo "No new java files detected"
exit 0
fi

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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
@ -69,7 +69,7 @@ jobs:
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() }}

View file

@ -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

View file

@ -14,9 +14,9 @@ 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
@ -26,17 +26,16 @@ jobs:
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View file

@ -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()
}
}

View file

@ -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

View file

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<FileEntity>
@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<FileEntity>
@Query("SELECT * FROM filelist WHERE file_owner = :fileOwner ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}")
fun getAllFiles(fileOwner: String): List<FileEntity>
@Query("SELECT * FROM filelist WHERE path LIKE :pathPattern AND file_owner = :fileOwner ORDER BY path ASC")
fun getFolderWithDescendants(pathPattern: String, fileOwner: String): List<FileEntity>
}

View file

@ -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)

View file

@ -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,43 +656,23 @@ 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<FileEntity> fileEntities =
fileDao.getFolderWithDescendants(oldPath + "%", user.getAccountName());
/// 2. prepare a batch of update operations to change all the descendants
ArrayList<ContentProviderOperation> operations = new ArrayList<>(cursor.getCount());
ArrayList<ContentProviderOperation> operations = new ArrayList<>(fileEntities.size());
String defaultSavePath = FileStorageUtils.getSavePath(user.getAccountName());
List<String> originalPathsToTriggerMediaScan = new ArrayList<>();
List<String> newPathsToTriggerMediaScan = new ArrayList<>();
if (cursor.moveToFirst()) {
int lengthOfOldPath = ocFile.getRemotePath().length();
int lengthOfOldPath = oldPath.length();
int lengthOfOldStoragePath = defaultSavePath.length() + lengthOfOldPath;
do {
for (FileEntity fileEntity: fileEntities) {
ContentValues contentValues = new ContentValues(); // keep construction in the loop
OCFile childFile = createFileInstance(cursor);
OCFile childFile = createFileInstance(fileEntity);
contentValues.put(
ProviderTableMeta.FILE_PATH,
targetPath + childFile.getRemotePath().substring(lengthOfOldPath)
@ -735,9 +709,7 @@ public class FileDataStorageManager {
.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<OCFile> getFolderContent(long parentId, boolean onlyOnDevice) {
Log_OC.d(TAG, "getFolderContent - start");
List<OCFile> 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;
}
} 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);
List<FileEntity> files = fileDao.getFolderContent(parentId);
for (FileEntity fileEntity: files) {
OCFile child = createFileInstance(fileEntity);
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,15 +865,21 @@ 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)));
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
@ -981,67 +890,64 @@ public class FileDataStorageManager {
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);
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)));
// }
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));
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(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)));
String sharees = cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_SHAREES));
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 = fileEntity.getSharees();
if (sharees == null || NULL_STRING.equals(sharees) || sharees.isEmpty()) {
ocFile.setSharees(new ArrayList<>());
} else {
try {
ShareeUser[] shareesArray = new Gson().fromJson(sharees, ShareeUser[].class);
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<>());
}
}
String metadataSize = cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_METADATA_SIZE));
ImageDimension imageDimension = new Gson().fromJson(metadataSize, ImageDimension.class);
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<OCFile> getGalleryItems(long startDate, long endDate) {
List<OCFile> files = new ArrayList<>();
Log_OC.d(TAG, "getGalleryItems - start: " + startDate + ", " + endDate);
Uri requestURI = ProviderTableMeta.CONTENT_URI;
Cursor cursor;
List<FileEntity> 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_CONTENT_TYPE + " LIKE ? OR " +
ProviderTableMeta.FILE_CONTENT_TYPE + " LIKE ? )",
new String[]{
user.getAccountName(),
String.valueOf(startDate),
String.valueOf(endDate),
"image/%",
"video/%"
},
null
);
} catch (RemoteException e) {
Log_OC.e(TAG, e.getMessage(), e);
return files;
}
} else {
cursor = getContentResolver().query(
requestURI,
null,
ProviderTableMeta.FILE_ACCOUNT_OWNER + AND +
ProviderTableMeta.FILE_MODIFIED + ">=? AND " +
ProviderTableMeta.FILE_MODIFIED + "<? AND (" +
ProviderTableMeta.FILE_CONTENT_TYPE + " LIKE ? OR " +
ProviderTableMeta.FILE_CONTENT_TYPE + " LIKE ? )",
new String[]{
user.getAccountName(),
String.valueOf(startDate),
String.valueOf(endDate),
"image/%",
"video/%"
},
null
);
}
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
OCFile child = createFileInstance(cursor);
files.add(child);
} while (cursor.moveToNext());
}
cursor.close();
List<OCFile> 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<OCFile> getAllFiles() {
String selection = ProviderTableMeta.FILE_ACCOUNT_OWNER + "= ? ";
String[] selectionArgs = new String[]{user.getAccountName()};
// TODO - Apparently this method is used only by tests
List<FileEntity> fileEntities = fileDao.getAllFiles(user.getAccountName());
List<OCFile> folderContent = new ArrayList<>(fileEntities.size());
List<OCFile> 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;

View file

@ -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<String>? = null
@Inject
lateinit var localBroadcastManager: LocalBroadcastManager
@ -95,8 +99,9 @@ open class FolderPickerActivity :
View.VISIBLE
findViewById<View>(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<Parcelable?>(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,8 +338,31 @@ open class FolderPickerActivity :
return
}
file = listOfFiles.currentFile
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
}
}
private fun updateNavigationElementsInActionBar() {
@ -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"

View file

@ -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);
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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)
}
}

View file

@ -69,15 +69,6 @@
android:theme="@style/Button.Primary"
app:cornerRadius="@dimen/button_corner_radius" />
<TextView
android:id="@+id/community_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/standard_half_padding"
android:paddingTop="@dimen/standard_half_padding"
android:text="@string/community_testing_version_text"
android:textAppearance="?android:attr/textAppearanceMedium"/>
<TextView
android:id="@+id/community_contribute_headline"
android:layout_width="wrap_content"

View file

@ -145,7 +145,7 @@
<string name="community_testing_bug_text">Fehler gefunden? Komisches Verhalten?</string>
<string name="community_testing_headline">Helfen Sie durch Testen</string>
<string name="community_testing_report_text">Fehlerbericht auf GitHub erstellen</string>
<string name="community_testing_version_text">Möchten Sie uns beim Testen der nächsten Version unterstützen?</string>
<string name="community_testing_version_text">Helfen Sie uns beim Testen der nächsten Version!</string>
<string name="configure_new_media_folder_detection_notifications">Konfigurieren</string>
<string name="confirm_removal">Lokale Verschlüsselung entfernen</string>
<string name="confirmation_remove_file_alert">Wollen Sie %1$s wirklich löschen?</string>

View file

@ -523,7 +523,6 @@
<string name="community_testing_headline">Help by testing</string>
<string name="community_testing_bug_text">Found a bug? Oddments?</string>
<string name="community_testing_report_text">Report an issue on GitHub</string>
<string name="community_testing_version_text">Interested in helping out by testing what will be the next version?</string>
<string name="community_beta_headline">Test the dev version</string>
<string name="community_beta_text">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.</string>
<string name="community_release_candidate_headline">Release candidate</string>

View file

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<Array<Any>> = 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<Array<Any>> = 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<Pair<String, String>> = 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<Array<Any>> {
val otherAncestors: Array<Array<Any>> = indirectAncestors.map {
@Suppress("UNCHECKED_CAST")
arrayOf(it.first, it.second, false) as Array<Any>
}.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<Array<Any>> {
val otherAncestors: Array<Array<Any>> = indirectAncestors.map {
@Suppress("UNCHECKED_CAST")
arrayOf(it.first, it.second, true) as Array<Any>
}.toTypedArray()
return directParents + nonAncestors + otherAncestors
}
}
}
}