Merge pull request #10584 from nextcloud/fix/syncedfolderutils-inconsistent-comparator

SyncedFolderUtils: solve inconsistent comparator crashes
This commit is contained in:
Álvaro Brey 2022-08-23 09:42:35 +02:00 committed by GitHub
commit 61c2dbb3d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 165 additions and 241 deletions

View file

@ -377,7 +377,7 @@ class SyncedFoldersActivity :
private fun createSyncedFolderWithoutMediaFolder(syncedFolder: SyncedFolder): SyncedFolderDisplayItem { private fun createSyncedFolderWithoutMediaFolder(syncedFolder: SyncedFolder): SyncedFolderDisplayItem {
val localFolder = File(syncedFolder.localPath) val localFolder = File(syncedFolder.localPath)
val files = SyncedFolderUtils.getFileList(localFolder) val files = SyncedFolderUtils.getFileList(localFolder)
val filePaths = getDisplayFilePathList(files.toList()) val filePaths = getDisplayFilePathList(files)
return SyncedFolderDisplayItem( return SyncedFolderDisplayItem(
syncedFolder.id, syncedFolder.id,
syncedFolder.localPath, syncedFolder.localPath,

View file

@ -1,240 +0,0 @@
/*
* Nextcloud Android client application
*
* @author Andy Scherzinger
* Copyright (C) 2020 Andy Scherzinger
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) 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.MediaFolder;
import com.owncloud.android.datamodel.MediaFolderType;
import com.owncloud.android.datamodel.SyncedFolder;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import androidx.annotation.Nullable;
/**
* Utility class with methods for processing synced folders.
*/
public final class SyncedFolderUtils {
private static final String[] DISQUALIFIED_MEDIA_DETECTION_SOURCE = new String[]{
"cover.jpg", "cover.jpeg",
"folder.jpg", "folder.jpeg"
};
private static final Set<String> DISQUALIFIED_MEDIA_DETECTION_FILE_SET =
new HashSet<>(Arrays.asList(DISQUALIFIED_MEDIA_DETECTION_SOURCE));
private static final Set<MediaFolderType> AUTO_QUALIFYING_FOLDER_TYPE_SET =
new HashSet<>(Collections.singletonList(MediaFolderType.CUSTOM));
private static final String THUMBNAIL_FOLDER_PREFIX = ".thumbnail";
private static final String THUMBNAIL_DATA_FILE_PREFIX = ".thumbdata";
private static final int SINGLE_FILE = 1;
private SyncedFolderUtils() {
// utility class -> private constructor
}
/**
* analyzes a given media folder if its content qualifies for the folder to be handled as a media folder.
*
* @param mediaFolder media folder to analyse
* @return <code>true</code> if it qualifies as a media folder else <code>false</code>
*/
public static boolean isQualifyingMediaFolder(@Nullable MediaFolder mediaFolder) {
if (mediaFolder == null) {
return false;
}
// custom folders are always fine
if (AUTO_QUALIFYING_FOLDER_TYPE_SET.contains(mediaFolder.type)) {
return true;
}
// thumbnail folder
if (!isQualifiedFolder(mediaFolder.absolutePath)) {
return false;
}
// filter media folders
// no files
if (mediaFolder.numberOfFiles < SINGLE_FILE) {
return false;
} // music album (just one cover-art image)
else if (MediaFolderType.IMAGE == mediaFolder.type) {
return containsQualifiedImages(mediaFolder.filePaths);
}
return true;
}
/**
* analyzes a given synced folder if its content qualifies for the folder to be handled as a media folder.
*
* @param syncedFolder synced folder to analyse
* @return <code>true</code> if it qualifies as a media folder else <code>false</code>
*/
public static boolean isQualifyingMediaFolder(@Nullable SyncedFolder syncedFolder) {
if (syncedFolder == null) {
return false;
}
// custom folders are always fine
if (AUTO_QUALIFYING_FOLDER_TYPE_SET.contains(syncedFolder.getType())) {
return true;
}
// thumbnail folder
if (!isQualifiedFolder(syncedFolder.getLocalPath())) {
return false;
}
// filter media folders
File[] files = getFileList(new File(syncedFolder.getLocalPath()));
// no files
if (files.length < SINGLE_FILE) {
return false;
} // music album (just one cover-art image)
else if (MediaFolderType.IMAGE == syncedFolder.getType()) {
return containsQualifiedImages(files);
}
return true;
}
/**
* analyzes a given folder based on a path-string and type if its content qualifies for the folder to be handled as
* a media folder.
*
* @param folderPath String representation for a folder
* @param folderType type of the folder
* @return <code>true</code> if it qualifies as a media folder else <code>false</code>
*/
public static boolean isQualifyingMediaFolder(String folderPath, MediaFolderType folderType) {
// custom folders are always fine
if (MediaFolderType.CUSTOM == folderType) {
return true;
}
// custom folders are always fine
if (AUTO_QUALIFYING_FOLDER_TYPE_SET.contains(folderType)) {
return true;
}
// thumbnail folder
if (!isQualifiedFolder(folderPath)) {
return false;
}
// filter media folders
File[] files = getFileList(new File(folderPath));
// no files
if (files.length < SINGLE_FILE) {
return false;
} // music album (just one cover-art image)
else if (MediaFolderType.IMAGE == folderType) {
return containsQualifiedImages(files);
}
return true;
}
/**
* check if folder is qualified for auto upload.
*
* @param folderPath the folder's path string
* @return code>true</code> if folder qualifies for auto upload else <code>false</code>
*/
public static boolean isQualifiedFolder(String folderPath) {
File folder = new File(folderPath);
// check if folder starts with thumbnail prefix
return folder.isDirectory() && !folder.getName().startsWith(THUMBNAIL_FOLDER_PREFIX);
}
/**
* check if given list contains images that qualify as auto upload relevant files.
*
* @param filePaths list of file paths
* @return <code>true</code> if at least one files qualifies as auto upload relevant else <code>false</code>
*/
private static boolean containsQualifiedImages(List<String> filePaths) {
for (String filePath : filePaths) {
if (isFileNameQualifiedForAutoUpload(FileUtil.getFilenameFromPathString(filePath))
&& MimeTypeUtil.isImage(MimeTypeUtil.getMimeTypeFromPath(filePath))) {
return true;
}
}
return false;
}
/**
* check if given list of files contains images that qualify as auto upload relevant files.
*
* @param files list of files
* @return <code>true</code> if at least one files qualifies as auto upload relevant else <code>false</code>
*/
private static boolean containsQualifiedImages(File... files) {
for (File file : files) {
if (isFileNameQualifiedForAutoUpload(file.getName()) && MimeTypeUtil.isImage(file)) {
return true;
}
}
return false;
}
/**
* check if given file is auto upload relevant files.
*
* @param fileName file name to be checked
* @return <code>true</code> if the file qualifies as auto upload relevant else <code>false</code>
*/
public static boolean isFileNameQualifiedForAutoUpload(String fileName) {
if (fileName != null) {
return !DISQUALIFIED_MEDIA_DETECTION_FILE_SET.contains(fileName.toLowerCase(Locale.ROOT))
&& !fileName.startsWith(THUMBNAIL_DATA_FILE_PREFIX);
} else {
return false;
}
}
/**
* return list of files for given folder.
*
* @param localFolder folder to scan
* @return sorted list of folder of given folder
*/
public static File[] getFileList(File localFolder) {
File[] files = localFolder.listFiles(pathname -> !pathname.isDirectory());
if (files != null) {
Arrays.sort(files, (f1, f2) -> Long.compare(f1.lastModified(), f2.lastModified()));
} else {
files = new File[]{};
}
return files;
}
}

View file

@ -0,0 +1,164 @@
/*
* Nextcloud Android client application
*
* @author Andy Scherzinger
* Copyright (C) 2020 Andy Scherzinger
* Copyright (C) 2022 Álvaro Brey
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) 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.MediaFolder
import com.owncloud.android.datamodel.MediaFolderType
import com.owncloud.android.datamodel.SyncedFolder
import java.io.File
/**
* Utility class with methods for processing synced folders.
*/
object SyncedFolderUtils {
private val DISQUALIFIED_MEDIA_DETECTION_SOURCE = listOf(
"cover.jpg",
"cover.jpeg",
"folder.jpg",
"folder.jpeg"
)
private val DISQUALIFIED_MEDIA_DETECTION_FILE_SET: Set<String> = DISQUALIFIED_MEDIA_DETECTION_SOURCE.toSet()
private val AUTO_QUALIFYING_FOLDER_TYPE_SET: Set<MediaFolderType> = setOf(MediaFolderType.CUSTOM)
private const val THUMBNAIL_FOLDER_PREFIX = ".thumbnail"
private const val THUMBNAIL_DATA_FILE_PREFIX = ".thumbdata"
private const val SINGLE_FILE = 1
/**
* analyzes a given media folder if its content qualifies for the folder to be handled as a media folder.
*
* @param mediaFolder media folder to analyse
* @return `true` if it qualifies as a media folder else `false`
*/
fun isQualifyingMediaFolder(mediaFolder: MediaFolder?): Boolean {
return when {
mediaFolder == null -> false
AUTO_QUALIFYING_FOLDER_TYPE_SET.contains(mediaFolder.type) -> true
!isQualifiedFolder(mediaFolder.absolutePath) -> false
else -> {
when {
mediaFolder.numberOfFiles < SINGLE_FILE -> false
// music album (just one cover-art image)
mediaFolder.type == MediaFolderType.IMAGE -> containsQualifiedImages(
mediaFolder.filePaths.map { File(it) }
)
else -> true
}
}
}
}
/**
* analyzes a given synced folder if its content qualifies for the folder to be handled as a media folder.
*
* @param syncedFolder synced folder to analyse
* @return `true` if it qualifies as a media folder else `false`
*/
fun isQualifyingMediaFolder(syncedFolder: SyncedFolder?): Boolean {
if (syncedFolder == null) {
return false
}
return isQualifyingMediaFolder(syncedFolder.localPath, syncedFolder.type)
}
/**
* analyzes a given folder based on a path-string and type if its content qualifies for the folder to be handled as
* a media folder.
*
* @param folderPath String representation for a folder
* @param folderType type of the folder
* @return `true` if it qualifies as a media folder else `false`
*/
fun isQualifyingMediaFolder(folderPath: String?, folderType: MediaFolderType): Boolean {
return when {
AUTO_QUALIFYING_FOLDER_TYPE_SET.contains(folderType) -> true
!isQualifiedFolder(folderPath) -> false
folderPath == null -> false
else -> {
val files: List<File> = getFileList(File(folderPath))
when {
// no files
files.size < SINGLE_FILE -> false
// music album (just one cover-art image)
folderType == MediaFolderType.IMAGE -> containsQualifiedImages(files)
else -> true
}
}
}
}
/**
* check if folder is qualified for auto upload.
*
* @param folderPath the folder's path string
* @return code>true if folder qualifies for auto upload else `false`
*/
@JvmStatic
fun isQualifiedFolder(folderPath: String?): Boolean {
if (folderPath == null) {
return false
}
val folder = File(folderPath)
// check if folder starts with thumbnail prefix
return folder.isDirectory && !folder.name.startsWith(THUMBNAIL_FOLDER_PREFIX)
}
/**
* check if given list of files contains images that qualify as auto upload relevant files.
*
* @param files list of files
* @return `true` if at least one files qualifies as auto upload relevant else `false`
*/
private fun containsQualifiedImages(files: List<File>): Boolean {
return files.any { isFileNameQualifiedForAutoUpload(it.name) && MimeTypeUtil.isImage(it) }
}
/**
* check if given file is auto upload relevant files.
*
* @param fileName file name to be checked
* @return `true` if the file qualifies as auto upload relevant else `false`
*/
@JvmStatic
fun isFileNameQualifiedForAutoUpload(fileName: String?): Boolean {
return when {
fileName != null -> {
!DISQUALIFIED_MEDIA_DETECTION_FILE_SET.contains(fileName.lowercase()) &&
!fileName.startsWith(THUMBNAIL_DATA_FILE_PREFIX)
}
else -> false
}
}
/**
* return list of files for given folder.
*
* @param localFolder folder to scan
* @return sorted list of folder of given folder
*/
fun getFileList(localFolder: File): List<File> {
val files: Array<File> = localFolder.listFiles { pathname: File -> !pathname.isDirectory } ?: return emptyList()
return files
.map { Pair(it, it.lastModified()) }
.sortedBy { it.second }
.map { it.first }
}
}