Browse Source

Merge pull request #7042 from nextcloud/enh/coverArtFilter

Auto Upload: Ignore folders that have no-images or seem to be music albums
Andy Scherzinger 4 years ago
parent
commit
ee01ed385e

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 344 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 337 warnings</span>

+ 74 - 0
src/androidTest/java/com/owncloud/android/utils/FileUtilTest.kt

@@ -0,0 +1,74 @@
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.utils
+
+import com.owncloud.android.AbstractIT
+import org.junit.Assert
+import org.junit.Test
+import java.io.File
+
+class FileUtilTest : AbstractIT() {
+    @Test
+    fun assertNullInput() {
+        Assert.assertEquals("", FileUtil.getFilenameFromPathString(null))
+    }
+
+    @Test
+    fun assertEmptyInput() {
+        Assert.assertEquals("", FileUtil.getFilenameFromPathString(""))
+    }
+
+    @Test
+    fun assertFileInput() {
+        val file = getDummyFile("empty.txt")
+        Assert.assertEquals("empty.txt", FileUtil.getFilenameFromPathString(file.absolutePath))
+    }
+
+    @Test
+    fun assertSlashInput() {
+        val tempPath = File(FileStorageUtils.getTemporalPath(account.name) + File.pathSeparator + "folder")
+        if (!tempPath.exists()) {
+            Assert.assertTrue(tempPath.mkdirs())
+        }
+        Assert.assertEquals("", FileUtil.getFilenameFromPathString(tempPath.absolutePath))
+    }
+
+    @Test
+    fun assertDotFileInput() {
+        val file = getDummyFile(".dotfile.ext")
+        Assert.assertEquals(".dotfile.ext", FileUtil.getFilenameFromPathString(file.absolutePath))
+    }
+
+    @Test
+    fun assertFolderInput() {
+        val tempPath = File(FileStorageUtils.getTemporalPath(account.name))
+        if (!tempPath.exists()) {
+            Assert.assertTrue(tempPath.mkdirs())
+        }
+
+        Assert.assertEquals("", FileUtil.getFilenameFromPathString(tempPath.absolutePath))
+    }
+
+    @Test
+    fun assertNoFileExtensionInput() {
+        val file = getDummyFile("file")
+        Assert.assertEquals("file", FileUtil.getFilenameFromPathString(file.absolutePath))
+    }
+}

+ 241 - 0
src/androidTest/java/com/owncloud/android/utils/SyncedFolderUtilsTest.kt

@@ -0,0 +1,241 @@
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.utils
+
+import com.owncloud.android.AbstractIT
+import com.owncloud.android.datamodel.MediaFolder
+import com.owncloud.android.datamodel.MediaFolderType
+import com.owncloud.android.datamodel.SyncedFolder
+import org.apache.commons.io.FileUtils
+import org.junit.AfterClass
+import org.junit.Assert
+import org.junit.BeforeClass
+import org.junit.Test
+import java.io.File
+import java.util.Arrays
+
+class SyncedFolderUtilsTest : AbstractIT() {
+    @Test
+    fun assertCoverFilenameUnqualified() {
+        Assert.assertFalse(SyncedFolderUtils.isFileNameQualifiedForAutoUpload(COVER))
+        Assert.assertFalse(SyncedFolderUtils.isFileNameQualifiedForAutoUpload("cover.JPG"))
+        Assert.assertFalse(SyncedFolderUtils.isFileNameQualifiedForAutoUpload("cover.jpeg"))
+        Assert.assertFalse(SyncedFolderUtils.isFileNameQualifiedForAutoUpload("cover.JPEG"))
+        Assert.assertFalse(SyncedFolderUtils.isFileNameQualifiedForAutoUpload("COVER.jpg"))
+        Assert.assertFalse(SyncedFolderUtils.isFileNameQualifiedForAutoUpload(FOLDER))
+        Assert.assertFalse(SyncedFolderUtils.isFileNameQualifiedForAutoUpload("Folder.jpeg"))
+        Assert.assertFalse(SyncedFolderUtils.isFileNameQualifiedForAutoUpload("FOLDER.jpg"))
+        Assert.assertFalse(SyncedFolderUtils.isFileNameQualifiedForAutoUpload(THUMBDATA_FILE))
+    }
+
+    @Test
+    fun assertImageFilenameQualified() {
+        Assert.assertTrue(SyncedFolderUtils.isFileNameQualifiedForAutoUpload("image.jpg"))
+        Assert.assertTrue(SyncedFolderUtils.isFileNameQualifiedForAutoUpload("screenshot.JPG"))
+        Assert.assertTrue(SyncedFolderUtils.isFileNameQualifiedForAutoUpload(IMAGE_JPEG))
+        Assert.assertTrue(SyncedFolderUtils.isFileNameQualifiedForAutoUpload("image.JPEG"))
+        Assert.assertTrue(SyncedFolderUtils.isFileNameQualifiedForAutoUpload("SCREENSHOT.jpg"))
+        Assert.assertTrue(SyncedFolderUtils.isFileNameQualifiedForAutoUpload(SELFIE))
+        Assert.assertTrue(SyncedFolderUtils.isFileNameQualifiedForAutoUpload("screenshot.PNG"))
+    }
+
+    @Test
+    fun assertMediaFolderNullSafe() {
+        val folder: MediaFolder? = null
+        Assert.assertFalse(SyncedFolderUtils.isQualifyingMediaFolder(folder))
+    }
+
+    @Test
+    fun assertMediaFolderCustomQualified() {
+        val folder = MediaFolder()
+        folder.type = MediaFolderType.CUSTOM
+        Assert.assertTrue(SyncedFolderUtils.isQualifyingMediaFolder(folder))
+    }
+
+    @Test
+    fun assertMediaFolderVideoUnqualified() {
+        val folder = MediaFolder()
+        folder.type = MediaFolderType.VIDEO
+        folder.numberOfFiles = 0L
+        Assert.assertFalse(SyncedFolderUtils.isQualifyingMediaFolder(folder))
+    }
+
+    @Test
+    fun assertMediaFolderVideoQualified() {
+        val folder = MediaFolder()
+        folder.type = MediaFolderType.VIDEO
+        folder.numberOfFiles = 20L
+        Assert.assertTrue(SyncedFolderUtils.isQualifyingMediaFolder(folder))
+    }
+
+    @Test
+    fun assertMediaFolderImagesQualified() {
+        val folder = MediaFolder()
+        folder.type = MediaFolderType.IMAGE
+        folder.numberOfFiles = 4L
+        folder.filePaths = Arrays.asList(
+            getDummyFile(SELFIE).absolutePath,
+            getDummyFile(SCREENSHOT).absolutePath,
+            getDummyFile(IMAGE_JPEG).absolutePath,
+            getDummyFile(IMAGE_BITMAP).absolutePath
+        )
+        Assert.assertTrue(SyncedFolderUtils.isQualifyingMediaFolder(folder))
+    }
+
+    @Test
+    fun assertMediaFolderImagesEmptyUnqualified() {
+        val folder = MediaFolder()
+        folder.type = MediaFolderType.IMAGE
+        folder.numberOfFiles = 0L
+        Assert.assertFalse(SyncedFolderUtils.isQualifyingMediaFolder(folder))
+    }
+
+    @Test
+    fun assertMediaFolderImagesNoImagesUnqualified() {
+        val folder = MediaFolder()
+        folder.type = MediaFolderType.IMAGE
+        folder.numberOfFiles = 3L
+        folder.filePaths = Arrays.asList(
+            getDummyFile(SONG_ZERO).absolutePath,
+            getDummyFile(SONG_ONE).absolutePath,
+            getDummyFile(SONG_TWO).absolutePath
+        )
+        Assert.assertFalse(SyncedFolderUtils.isQualifyingMediaFolder(folder))
+    }
+
+    @Test
+    fun assertMediaFolderImagesMusicAlbumWithCoverArtUnqualified() {
+        val folder = MediaFolder()
+        folder.type = MediaFolderType.IMAGE
+        folder.numberOfFiles = 3L
+        folder.filePaths = Arrays.asList(
+            getDummyFile(COVER).absolutePath,
+            getDummyFile(SONG_ONE).absolutePath,
+            getDummyFile(SONG_TWO).absolutePath
+        )
+        Assert.assertFalse(SyncedFolderUtils.isQualifyingMediaFolder(folder))
+    }
+
+    @Test
+    fun assertMediaFolderImagesMusicAlbumWithFolderArtUnqualified() {
+        val folder = MediaFolder()
+        folder.type = MediaFolderType.IMAGE
+        folder.numberOfFiles = 3L
+        folder.filePaths = Arrays.asList(
+            getDummyFile(FOLDER).absolutePath,
+            getDummyFile(SONG_ONE).absolutePath,
+            getDummyFile(SONG_TWO).absolutePath
+        )
+        Assert.assertFalse(SyncedFolderUtils.isQualifyingMediaFolder(folder))
+    }
+
+    @Test
+    fun assertSyncedFolderNullSafe() {
+        val folder: SyncedFolder? = null
+        Assert.assertFalse(SyncedFolderUtils.isQualifyingMediaFolder(folder))
+    }
+
+    @Test
+    fun assertUnqualifiedContentSyncedFolder() {
+        getDummyFile(THUMBDATA_FOLDER + File.pathSeparator + THUMBDATA_FILE)
+        val folder = SyncedFolder(
+            FileStorageUtils.getTemporalPath(account.name) + File.pathSeparator + THUMBDATA_FOLDER,
+            "",
+            true,
+            false,
+            false,
+            true,
+            account.name,
+            1,
+            1,
+            true,
+            0L,
+            MediaFolderType.IMAGE,
+            false
+        )
+        Assert.assertFalse(SyncedFolderUtils.isQualifyingMediaFolder(folder))
+    }
+
+    @Test
+    fun assertUnqualifiedSyncedFolder() {
+        getDummyFile(THUMBNAILS_FOLDER + File.pathSeparator + IMAGE_JPEG)
+        getDummyFile(THUMBNAILS_FOLDER + File.pathSeparator + IMAGE_BITMAP)
+        val folder = SyncedFolder(
+            FileStorageUtils.getTemporalPath(account.name) + File.pathSeparator + THUMBNAILS_FOLDER,
+            "",
+            true,
+            false,
+            false,
+            true,
+            account.name,
+            1,
+            1,
+            true,
+            0L,
+            MediaFolderType.IMAGE,
+            false
+        )
+        Assert.assertFalse(SyncedFolderUtils.isQualifyingMediaFolder(folder))
+    }
+
+    companion object {
+        private const val SELFIE = "selfie.png"
+        private const val SCREENSHOT = "screenshot.JPG"
+        private const val IMAGE_JPEG = "image.jpeg"
+        private const val IMAGE_BITMAP = "image.bmp"
+        private const val SONG_ZERO = "song0.mp3"
+        private const val SONG_ONE = "song1.mp3"
+        private const val SONG_TWO = "song2.mp3"
+        private const val FOLDER = "folder.JPG"
+        private const val COVER = "cover.jpg"
+        private const val THUMBNAILS_FOLDER = ".thumbnails"
+        private const val THUMBDATA_FOLDER = "valid_folder"
+        private const val THUMBDATA_FILE = ".thumbdata4--1967290299"
+        private const val ITERATION = 100
+
+        @BeforeClass
+        fun setUp() {
+            val tempPath = File(FileStorageUtils.getTemporalPath(account.name) + File.pathSeparator + THUMBNAILS_FOLDER)
+            if (!tempPath.exists()) {
+                tempPath.mkdirs()
+            }
+
+            createFile(SELFIE, ITERATION)
+            createFile(SCREENSHOT, ITERATION)
+            createFile(IMAGE_JPEG, ITERATION)
+            createFile(IMAGE_BITMAP, ITERATION)
+            createFile(SONG_ZERO, ITERATION)
+            createFile(SONG_ONE, ITERATION)
+            createFile(SONG_TWO, ITERATION)
+            createFile(FOLDER, ITERATION)
+            createFile(COVER, ITERATION)
+
+            createFile(THUMBDATA_FOLDER + File.pathSeparator + THUMBDATA_FILE, ITERATION)
+            createFile(THUMBNAILS_FOLDER + File.pathSeparator + IMAGE_JPEG, ITERATION)
+            createFile(THUMBNAILS_FOLDER + File.pathSeparator + IMAGE_BITMAP, ITERATION)
+        }
+
+        @AfterClass
+        fun tearDown() {
+            FileUtils.deleteDirectory(File(FileStorageUtils.getTemporalPath(account.name)))
+        }
+    }
+}

+ 7 - 3
src/main/java/com/nextcloud/client/jobs/MediaFoldersDetectionWork.kt

@@ -45,6 +45,7 @@ import com.nextcloud.client.preferences.AppPreferences
 import com.nextcloud.client.preferences.AppPreferencesImpl
 import com.owncloud.android.R
 import com.owncloud.android.datamodel.ArbitraryDataProvider
+import com.owncloud.android.datamodel.MediaFolderType
 import com.owncloud.android.datamodel.MediaFoldersModel
 import com.owncloud.android.datamodel.MediaProvider
 import com.owncloud.android.datamodel.SyncedFolderProvider
@@ -52,6 +53,7 @@ import com.owncloud.android.lib.common.utils.Log_OC
 import com.owncloud.android.ui.activity.ManageAccountsActivity.PENDING_FOR_REMOVAL
 import com.owncloud.android.ui.activity.SyncedFoldersActivity
 import com.owncloud.android.ui.notifications.NotificationUtils
+import com.owncloud.android.utils.SyncedFolderUtils
 import com.owncloud.android.utils.ThemeUtils
 import java.util.ArrayList
 import java.util.Random
@@ -133,7 +135,9 @@ class MediaFoldersDetectionWork constructor(
                                 imageMediaFolder,
                                 user.toPlatformAccount()
                             )
-                            if (folder == null) {
+                            if (folder == null &&
+                                SyncedFolderUtils.isQualifyingMediaFolder(imageMediaFolder, MediaFolderType.IMAGE)
+                            ) {
                                 val contentTitle = String.format(
                                     resources.getString(R.string.new_media_folder_detected),
                                     resources.getString(R.string.new_media_folder_photos)
@@ -143,7 +147,7 @@ class MediaFoldersDetectionWork constructor(
                                     imageMediaFolder.substring(imageMediaFolder.lastIndexOf('/') + 1),
                                     user,
                                     imageMediaFolder,
-                                    1
+                                    MediaFolderType.IMAGE.id
                                 )
                             }
                         }
@@ -162,7 +166,7 @@ class MediaFoldersDetectionWork constructor(
                                     videoMediaFolder.substring(videoMediaFolder.lastIndexOf('/') + 1),
                                     user,
                                     videoMediaFolder,
-                                    2
+                                    MediaFolderType.VIDEO.id
                                 )
                             }
                         }

+ 8 - 2
src/main/java/com/owncloud/android/datamodel/FilesystemDataProvider.java

@@ -26,6 +26,7 @@ import android.net.Uri;
 
 import com.owncloud.android.db.ProviderMeta;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.utils.SyncedFolderUtils;
 
 import java.io.BufferedInputStream;
 import java.io.File;
@@ -96,8 +97,13 @@ public class FilesystemDataProvider {
                     if (value == null) {
                         Log_OC.e(TAG, "Cannot get local path");
                     } else {
-                        if (".thumbnail".equals(new File(value).getName())) {
-                            Log_OC.d(TAG, "Ignoring file for upload: " + value);
+                        File file = new File(value);
+                        if (!file.exists()) {
+                            Log_OC.d(TAG, "Ignoring file for upload (doesn't exist): " + value);
+                        } else if (SyncedFolderUtils.isQualifiedFolder(file.getParent())) {
+                            Log_OC.d(TAG, "Ignoring file for upload (unqualified folder): " + value);
+                        } else if (SyncedFolderUtils.isFileNameQualifiedForAutoUpload(file.getName())) {
+                            Log_OC.d(TAG, "Ignoring file for upload (unqualified file): " + value);
                         } else {
                             localPathsToUpload.add(value);
                         }

+ 46 - 86
src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java

@@ -38,12 +38,7 @@ import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.ProgressBar;
-import android.widget.TextView;
 
-import com.google.android.material.button.MaterialButton;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.core.Clock;
 import com.nextcloud.client.device.PowerManagementService;
@@ -56,6 +51,7 @@ import com.nextcloud.java.util.Optional;
 import com.owncloud.android.BuildConfig;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
+import com.owncloud.android.databinding.SyncedFoldersLayoutBinding;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.MediaFolder;
 import com.owncloud.android.datamodel.MediaFolderType;
@@ -71,11 +67,11 @@ import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment;
 import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.PermissionUtil;
+import com.owncloud.android.utils.SyncedFolderUtils;
 import com.owncloud.android.utils.ThemeUtils;
 
 import java.io.File;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -91,10 +87,6 @@ import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentTransaction;
 import androidx.lifecycle.Lifecycle;
 import androidx.recyclerview.widget.GridLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import butterknife.OnClick;
 
 import static android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS;
 import static com.owncloud.android.datamodel.SyncedFolderDisplayItem.UNPERSISTED_ID;
@@ -110,27 +102,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
     private static final String SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG";
     private static final String TAG = SyncedFoldersActivity.class.getSimpleName();
 
-    @BindView(R.id.empty_list_view)
-    public LinearLayout emptyContentContainer;
-
-    @BindView(R.id.empty_list_icon)
-    public ImageView emptyContentIcon;
-
-    @BindView(R.id.empty_list_progress)
-    public ProgressBar emptyContentProgressBar;
-
-    @BindView(R.id.empty_list_view_headline)
-    public TextView emptyContentHeadline;
-
-    @BindView(R.id.empty_list_view_text)
-    public TextView emptyContentMessage;
-
-    @BindView(R.id.empty_list_view_action)
-    public MaterialButton emptyContentActionButton;
-
-    @BindView(android.R.id.list)
-    public RecyclerView mRecyclerView;
-
+    private SyncedFoldersLayoutBinding binding;
     private SyncedFolderAdapter adapter;
     private SyncedFolderProvider syncedFolderProvider;
     private SyncedFolderPreferencesDialogFragment syncedFolderPreferencesDialogFragment;
@@ -151,8 +123,8 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
             showSidebar = getIntent().getExtras().getBoolean(EXTRA_SHOW_SIDEBAR);
         }
 
-        setContentView(R.layout.synced_folders_layout);
-        ButterKnife.bind(this);
+        binding = SyncedFoldersLayoutBinding.inflate(getLayoutInflater());
+        setContentView(binding.getRoot());
 
         if (getIntent() != null && getIntent().getExtras() != null) {
             final String accountName = getIntent().getExtras().getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT);
@@ -192,6 +164,8 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
         if (ThemeUtils.themingEnabled(this)) {
             setTheme(R.style.FallbackThemingTheme);
         }
+
+        binding.emptyList.emptyListViewAction.setOnClickListener(v -> showHiddenItems());
     }
 
     @Override
@@ -241,25 +215,24 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
         boolean lightVersion = getResources().getBoolean(R.bool.syncedFolder_light);
         adapter = new SyncedFolderAdapter(this, clock, gridWidth, this, lightVersion);
         syncedFolderProvider = new SyncedFolderProvider(getContentResolver(), preferences, clock);
-        emptyContentIcon.setImageResource(R.drawable.nav_synced_folders);
-        ThemeUtils.colorPrimaryButton(emptyContentActionButton, this);
+        binding.emptyList.emptyListIcon.setImageResource(R.drawable.nav_synced_folders);
+        ThemeUtils.colorPrimaryButton(binding.emptyList.emptyListViewAction, this);
 
         final GridLayoutManager lm = new GridLayoutManager(this, gridWidth);
         adapter.setLayoutManager(lm);
         int spacing = getResources().getDimensionPixelSize(R.dimen.media_grid_spacing);
-        mRecyclerView.addItemDecoration(new MediaGridItemDecoration(spacing));
-        mRecyclerView.setLayoutManager(lm);
-        mRecyclerView.setAdapter(adapter);
+        binding.list.addItemDecoration(new MediaGridItemDecoration(spacing));
+        binding.list.setLayoutManager(lm);
+        binding.list.setAdapter(adapter);
 
         load(gridWidth * 2, false);
     }
 
-    @OnClick(R.id.empty_list_view_action)
     public void showHiddenItems() {
         if (adapter.getSectionCount() == 0 && adapter.getUnfilteredSectionCount() > adapter.getSectionCount()) {
             adapter.toggleHiddenItemsVisibility();
-            emptyContentContainer.setVisibility(View.GONE);
-            mRecyclerView.setVisibility(View.VISIBLE);
+            binding.emptyList.emptyListView.setVisibility(View.GONE);
+            binding.list.setVisibility(View.VISIBLE);
         }
     }
 
@@ -378,13 +351,17 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
                 SyncedFolder syncedFolder = syncedFoldersMap.get(mediaFolder.absolutePath + "-" + mediaFolder.type);
                 syncedFoldersMap.remove(mediaFolder.absolutePath + "-" + mediaFolder.type);
 
-                if (MediaFolderType.CUSTOM == syncedFolder.getType()) {
-                    result.add(createSyncedFolderWithoutMediaFolder(syncedFolder));
-                } else {
-                    result.add(createSyncedFolder(syncedFolder, mediaFolder));
+                if (syncedFolder != null && SyncedFolderUtils.isQualifyingMediaFolder(syncedFolder)) {
+                    if (MediaFolderType.CUSTOM == syncedFolder.getType()) {
+                        result.add(createSyncedFolderWithoutMediaFolder(syncedFolder));
+                    } else {
+                        result.add(createSyncedFolder(syncedFolder, mediaFolder));
+                    }
                 }
             } else {
-                result.add(createSyncedFolderFromMediaFolder(mediaFolder));
+                if (SyncedFolderUtils.isQualifyingMediaFolder(mediaFolder)) {
+                    result.add(createSyncedFolderFromMediaFolder(mediaFolder));
+                }
             }
         }
 
@@ -399,7 +376,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
     private SyncedFolderDisplayItem createSyncedFolderWithoutMediaFolder(@NonNull SyncedFolder syncedFolder) {
 
         File localFolder = new File(syncedFolder.getLocalPath());
-        File[] files = getFileList(localFolder);
+        File[] files = SyncedFolderUtils.getFileList(localFolder);
         List<String> filePaths = getDisplayFilePathList(files);
 
         return new SyncedFolderDisplayItem(
@@ -479,18 +456,6 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
                 false);
     }
 
-    private 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;
-    }
-
     private List<String> getDisplayFilePathList(File... files) {
         List<String> filePaths = null;
 
@@ -525,17 +490,14 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
      * show recycler view list or the empty message info (in case list is empty).
      */
     private void showList() {
-        if (mRecyclerView != null) {
-            mRecyclerView.setVisibility(View.VISIBLE);
-            emptyContentProgressBar.setVisibility(View.GONE);
-
-            checkAndShowEmptyListContent();
-        }
+        binding.list.setVisibility(View.VISIBLE);
+        binding.emptyList.emptyListProgress.setVisibility(View.GONE);
+        checkAndShowEmptyListContent();
     }
 
     private void checkAndShowEmptyListContent() {
         if (adapter.getSectionCount() == 0 && adapter.getUnfilteredSectionCount() > adapter.getSectionCount()) {
-            emptyContentContainer.setVisibility(View.VISIBLE);
+            binding.emptyList.emptyListView.setVisibility(View.VISIBLE);
             int hiddenFoldersCount = adapter.getHiddenFolderCount();
 
             showEmptyContent(getString(R.string.drawer_synced_folders),
@@ -546,11 +508,11 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
                                                               hiddenFoldersCount,
                                                               hiddenFoldersCount));
         } else if (adapter.getSectionCount() == 0 && adapter.getUnfilteredSectionCount() == 0) {
-            emptyContentContainer.setVisibility(View.VISIBLE);
+            binding.emptyList.emptyListView.setVisibility(View.VISIBLE);
             showEmptyContent(getString(R.string.drawer_synced_folders),
                              getString(R.string.synced_folders_no_results));
         } else {
-            emptyContentContainer.setVisibility(View.GONE);
+            binding.emptyList.emptyListView.setVisibility(View.GONE);
         }
     }
 
@@ -642,14 +604,14 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
 
     private void showEmptyContent(String headline, String message) {
         showEmptyContent(headline, message, false);
-        emptyContentActionButton.setVisibility(View.GONE);
+        binding.emptyList.emptyListViewAction.setVisibility(View.GONE);
     }
 
     private void showEmptyContent(String headline, String message, String action) {
         showEmptyContent(headline, message, false);
-        emptyContentActionButton.setText(action);
-        emptyContentActionButton.setVisibility(View.VISIBLE);
-        emptyContentMessage.setVisibility(View.GONE);
+        binding.emptyList.emptyListViewAction.setText(action);
+        binding.emptyList.emptyListViewAction.setVisibility(View.VISIBLE);
+        binding.emptyList.emptyListViewText.setVisibility(View.GONE);
     }
 
     private void showLoadingContent() {
@@ -658,25 +620,23 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
             getString(R.string.synced_folders_loading_folders),
             true
         );
-        emptyContentActionButton.setVisibility(View.GONE);
+        binding.emptyList.emptyListViewAction.setVisibility(View.GONE);
     }
 
     private void showEmptyContent(String headline, String message, boolean loading) {
-        if (emptyContentContainer != null) {
-            emptyContentContainer.setVisibility(View.VISIBLE);
-            mRecyclerView.setVisibility(View.GONE);
+        binding.emptyList.emptyListView.setVisibility(View.VISIBLE);
+        binding.list.setVisibility(View.GONE);
 
-            emptyContentHeadline.setText(headline);
-            emptyContentMessage.setText(message);
-            emptyContentMessage.setVisibility(View.VISIBLE);
+        binding.emptyList.emptyListViewHeadline.setText(headline);
+        binding.emptyList.emptyListViewText.setText(message);
+        binding.emptyList.emptyListViewText.setVisibility(View.VISIBLE);
 
-            if (loading) {
-                emptyContentProgressBar.setVisibility(View.VISIBLE);
-                emptyContentIcon.setVisibility(View.GONE);
-            } else {
-                emptyContentProgressBar.setVisibility(View.GONE);
-                emptyContentIcon.setVisibility(View.VISIBLE);
-            }
+        if (loading) {
+            binding.emptyList.emptyListProgress.setVisibility(View.VISIBLE);
+            binding.emptyList.emptyListIcon.setVisibility(View.GONE);
+        } else {
+            binding.emptyList.emptyListProgress.setVisibility(View.GONE);
+            binding.emptyList.emptyListIcon.setVisibility(View.VISIBLE);
         }
     }
 

+ 55 - 0
src/main/java/com/owncloud/android/utils/FileUtil.java

@@ -0,0 +1,55 @@
+/*
+ * 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 android.text.TextUtils;
+
+import java.io.File;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public final class FileUtil {
+
+    private FileUtil() {
+        // utility class -> private constructor
+    }
+
+    /**
+     * returns the file name of a given path.
+     *
+     * @param filePath (absolute) file path
+     * @return the filename including its file extension, <code>empty String</code> for invalid input values
+     */
+    public static @NonNull
+    String getFilenameFromPathString(@Nullable String filePath) {
+        if (!TextUtils.isEmpty(filePath)) {
+            File file = new File(filePath);
+            if (file.isFile()) {
+                return file.getName();
+            } else {
+                return "";
+            }
+        } else {
+            return "";
+        }
+    }
+}

+ 240 - 0
src/main/java/com/owncloud/android/utils/SyncedFolderUtils.java

@@ -0,0 +1,240 @@
+/*
+ * 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 praefix
+        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;
+    }
+}

+ 3 - 1
src/main/res/layout/synced_folders_layout.xml

@@ -40,7 +40,9 @@
             android:orientation="vertical"
             app:layout_behavior="@string/appbar_scrolling_view_behavior">
 
-            <include layout="@layout/empty_list" />
+            <include
+                android:id="@+id/emptyList"
+                layout="@layout/empty_list" />
 
             <androidx.recyclerview.widget.RecyclerView
                 android:id="@android:id/list"