Explorar o código

add exclude hidden file or folder option when create custom media folder type

Signed-off-by: JinWeiyang <jwy8645@163.com>
JinWeiyang hai 1 ano
pai
achega
82fc3cf217

+ 2 - 1
app/src/androidTest/java/com/nextcloud/client/SyncedFoldersActivityIT.java

@@ -74,7 +74,8 @@ public class SyncedFoldersActivityIT extends AbstractIT {
                                                                    "Name",
                                                                    MediaFolderType.IMAGE,
                                                                    false,
-                                                                   SubFolderRule.YEAR_MONTH);
+                                                                   SubFolderRule.YEAR_MONTH,
+                                                                   false);
         SyncedFolderPreferencesDialogFragment sut = SyncedFolderPreferencesDialogFragment.newInstance(item, 0);
 
         Intent intent = new Intent(targetContext, SyncedFoldersActivity.class);

+ 4 - 2
app/src/androidTest/java/com/owncloud/android/utils/SyncedFolderUtilsTest.kt

@@ -187,7 +187,8 @@ class SyncedFolderUtilsTest : AbstractIT() {
             0L,
             MediaFolderType.IMAGE,
             false,
-            SubFolderRule.YEAR_MONTH
+            SubFolderRule.YEAR_MONTH,
+            false
         )
         Assert.assertFalse(SyncedFolderUtils.isQualifyingMediaFolder(folder))
     }
@@ -210,7 +211,8 @@ class SyncedFolderUtilsTest : AbstractIT() {
             0L,
             MediaFolderType.IMAGE,
             false,
-            SubFolderRule.YEAR_MONTH
+            SubFolderRule.YEAR_MONTH,
+            false
         )
         Assert.assertFalse(SyncedFolderUtils.isQualifyingMediaFolder(folder))
     }

+ 3 - 1
app/src/main/java/com/nextcloud/client/database/entity/SyncedFolderEntity.kt

@@ -59,5 +59,7 @@ data class SyncedFolderEntity(
     @ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_HIDDEN)
     val hidden: Int?,
     @ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_RULE)
-    val subFolderRule: Int?
+    val subFolderRule: Int?,
+    @ColumnInfo(name = ProviderTableMeta.SYNCED_EXCLUDE_HIDDEN)
+    val excludeHidden: Int?
 )

+ 18 - 3
app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java

@@ -51,6 +51,7 @@ public class SyncedFolder implements Serializable, Cloneable {
     private MediaFolderType type;
     private boolean hidden;
     private SubFolderRule subfolderRule;
+    private boolean excludeHidden;
 
     /**
      * constructor for new, to be persisted entity.
@@ -68,6 +69,8 @@ public class SyncedFolder implements Serializable, Cloneable {
      * @param timestampMs         the current timestamp in milliseconds
      * @param type                the type of the folder
      * @param hidden              hide item flag
+     * @param subFolderRule   whether to filter subFolder by year/month/day
+     * @param excludeHidden   exclude hidden file or folder, for {@link MediaFolderType#CUSTOM} only
      */
     public SyncedFolder(String localPath,
                         String remotePath,
@@ -82,7 +85,8 @@ public class SyncedFolder implements Serializable, Cloneable {
                         long timestampMs,
                         MediaFolderType type,
                         boolean hidden,
-                        SubFolderRule subFolderRule) {
+                        SubFolderRule subFolderRule,
+                        boolean excludeHidden) {
         this(UNPERSISTED_ID,
              localPath,
              remotePath,
@@ -97,7 +101,8 @@ public class SyncedFolder implements Serializable, Cloneable {
              timestampMs,
              type,
              hidden,
-             subFolderRule);
+             subFolderRule,
+             excludeHidden);
     }
 
     /**
@@ -119,7 +124,8 @@ public class SyncedFolder implements Serializable, Cloneable {
                            long timestampMs,
                            MediaFolderType type,
                            boolean hidden,
-                           SubFolderRule subFolderRule) {
+                           SubFolderRule subFolderRule,
+                           boolean excludeHidden) {
         this.id = id;
         this.localPath = localPath;
         this.remotePath = remotePath;
@@ -134,6 +140,7 @@ public class SyncedFolder implements Serializable, Cloneable {
         this.type = type;
         this.hidden = hidden;
         this.subfolderRule = subFolderRule;
+        this.excludeHidden = excludeHidden;
     }
 
     /**
@@ -263,4 +270,12 @@ public class SyncedFolder implements Serializable, Cloneable {
     }
 
     public void setSubFolderRule(SubFolderRule subFolderRule) { this.subfolderRule = subFolderRule; }
+
+    public boolean isExcludeHidden() {
+        return excludeHidden;
+    }
+
+    public void setExcludeHidden(boolean excludeHidden) {
+        this.excludeHidden = excludeHidden;
+    }
 }

+ 9 - 4
app/src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java

@@ -54,6 +54,7 @@ public class SyncedFolderDisplayItem extends SyncedFolder {
      * @param type            the type of the folder
      * @param hidden          hide item flag
      * @param subFolderRule   whether to filter subFolder by year/month/day
+     * @param excludeHidden   exclude hidden file or folder, for {@link MediaFolderType#CUSTOM} only
      */
     public SyncedFolderDisplayItem(long id,
                                    String localPath,
@@ -72,7 +73,8 @@ public class SyncedFolderDisplayItem extends SyncedFolder {
                                    long numberOfFiles,
                                    MediaFolderType type,
                                    boolean hidden,
-                                   SubFolderRule subFolderRule) {
+                                   SubFolderRule subFolderRule,
+                                   boolean excludeHidden) {
         super(id,
               localPath,
               remotePath,
@@ -87,7 +89,8 @@ public class SyncedFolderDisplayItem extends SyncedFolder {
               timestampMs,
               type,
               hidden,
-              subFolderRule);
+              subFolderRule,
+              excludeHidden);
         this.filePaths = filePaths;
         this.folderName = folderName;
         this.numberOfFiles = numberOfFiles;
@@ -108,7 +111,8 @@ public class SyncedFolderDisplayItem extends SyncedFolder {
                                    String folderName,
                                    MediaFolderType type,
                                    boolean hidden,
-                                   SubFolderRule subFolderRule) {
+                                   SubFolderRule subFolderRule,
+                                   boolean excludeHidden) {
         super(id,
               localPath,
               remotePath,
@@ -123,7 +127,8 @@ public class SyncedFolderDisplayItem extends SyncedFolder {
               timestampMs,
               type,
               hidden,
-              subFolderRule);
+              subFolderRule,
+              excludeHidden);
         this.folderName = folderName;
     }
 

+ 5 - 1
app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java

@@ -372,6 +372,8 @@ public class SyncedFolderProvider extends Observable {
                 ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_HIDDEN)) == 1;
             SubFolderRule subFolderRule = SubFolderRule.values()[cursor.getInt(
                     cursor.getColumnIndexOrThrow(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_RULE))];
+            boolean excludeHidden = cursor.getInt(cursor.getColumnIndexOrThrow(
+                ProviderMeta.ProviderTableMeta.SYNCED_EXCLUDE_HIDDEN)) == 1;
 
 
             syncedFolder = new SyncedFolder(id,
@@ -388,7 +390,8 @@ public class SyncedFolderProvider extends Observable {
                                             enabledTimestampMs,
                                             type,
                                             hidden,
-                                            subFolderRule);
+                                            subFolderRule,
+                                            excludeHidden);
         }
         return syncedFolder;
     }
@@ -417,6 +420,7 @@ public class SyncedFolderProvider extends Observable {
         cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_TYPE, syncedFolder.getType().id);
         cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_HIDDEN, syncedFolder.isHidden());
         cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_RULE, syncedFolder.getSubfolderRule().ordinal());
+        cv.put(ProviderMeta.ProviderTableMeta.SYNCED_EXCLUDE_HIDDEN, syncedFolder.isExcludeHidden());
 
         return cv;
     }

+ 1 - 0
app/src/main/java/com/owncloud/android/db/ProviderMeta.java

@@ -302,6 +302,7 @@ public class ProviderMeta {
         public static final String SYNCED_FOLDER_NAME_COLLISION_POLICY = "name_collision_policy";
         public static final String SYNCED_FOLDER_HIDDEN = "hidden";
         public static final String SYNCED_FOLDER_SUBFOLDER_RULE = "sub_folder_rule";
+        public static final String SYNCED_EXCLUDE_HIDDEN = "exclude_hidden";
 
         // Columns of external links table
         public static final String EXTERNAL_LINKS_ICON_URL = "icon_url";

+ 16 - 7
app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt

@@ -403,7 +403,8 @@ class SyncedFoldersActivity :
             files.size.toLong(),
             syncedFolder.type,
             syncedFolder.isHidden,
-            syncedFolder.subfolderRule
+            syncedFolder.subfolderRule,
+            syncedFolder.isExcludeHidden
         )
     }
 
@@ -433,7 +434,8 @@ class SyncedFoldersActivity :
             mediaFolder.numberOfFiles,
             mediaFolder.type,
             syncedFolder.isHidden,
-            syncedFolder.subfolderRule
+            syncedFolder.subfolderRule,
+            syncedFolder.isExcludeHidden
         )
     }
 
@@ -462,7 +464,8 @@ class SyncedFoldersActivity :
             mediaFolder.numberOfFiles,
             mediaFolder.type,
             false,
-            SubFolderRule.YEAR_MONTH
+            SubFolderRule.YEAR_MONTH,
+            false
         )
     }
 
@@ -554,7 +557,8 @@ class SyncedFoldersActivity :
                         null,
                         MediaFolderType.CUSTOM,
                         false,
-                        SubFolderRule.YEAR_MONTH
+                        SubFolderRule.YEAR_MONTH,
+                        false
                     )
                     onSyncFolderSettingsClick(0, emptyCustomFolder)
                 } else {
@@ -670,7 +674,8 @@ class SyncedFoldersActivity :
                 File(syncedFolder.localPath).name,
                 syncedFolder.type,
                 syncedFolder.isHidden,
-                syncedFolder.subFolderRule
+                syncedFolder.subFolderRule,
+                syncedFolder.isExcludeHidden
             )
             saveOrUpdateSyncedFolder(newCustomFolder)
             adapter.addSyncFolderItem(newCustomFolder)
@@ -688,7 +693,8 @@ class SyncedFoldersActivity :
                 syncedFolder.uploadAction,
                 syncedFolder.nameCollisionPolicy.serialize(),
                 syncedFolder.isEnabled,
-                syncedFolder.subFolderRule
+                syncedFolder.subFolderRule,
+                syncedFolder.isExcludeHidden
             )
             saveOrUpdateSyncedFolder(item)
 
@@ -759,6 +765,7 @@ class SyncedFoldersActivity :
      * @param uploadAction    upload action
      * @param nameCollisionPolicy what to do on name collision
      * @param enabled         is sync enabled
+     * @param excludeHidden   exclude hidden file or folder, for {@link MediaFolderType#CUSTOM} only
      */
     @Suppress("LongParameterList")
     private fun updateSyncedFolderItem(
@@ -773,7 +780,8 @@ class SyncedFoldersActivity :
         uploadAction: Int,
         nameCollisionPolicy: Int,
         enabled: Boolean,
-        subFolderRule: SubFolderRule
+        subFolderRule: SubFolderRule,
+        excludeHidden: Boolean
     ) {
         item.id = id
         item.localPath = localPath
@@ -786,6 +794,7 @@ class SyncedFoldersActivity :
         item.setNameCollisionPolicy(nameCollisionPolicy)
         item.setEnabled(enabled, clock.currentTime)
         item.setSubFolderRule(subFolderRule)
+        item.setExcludeHidden(excludeHidden)
     }
 
     override fun onRequestPermissionsResult(

+ 18 - 1
app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.kt

@@ -130,12 +130,16 @@ class SyncedFolderPreferencesDialogFragment : DialogFragment(), Injectable {
             // hide local folder chooser and delete for non-custom folders
             binding.localFolderContainer.visibility = View.GONE
             isNeutralButtonActive = false
+            binding.settingInstantUploadExcludeHiddenContainer.visibility = View.GONE
         } else if (syncedFolder!!.id <= SyncedFolder.UNPERSISTED_ID) {
             isNeutralButtonActive = false
 
             // Hide delete/enabled for unpersisted custom folders
             binding.syncEnabled.visibility = View.GONE
 
+            // Show exclude hidden checkbox when {@link MediaFolderType#CUSTOM}
+            binding.settingInstantUploadExcludeHiddenContainer.visibility = View.VISIBLE
+
             // auto set custom folder to enabled
             syncedFolder?.isEnabled = true
 
@@ -146,6 +150,10 @@ class SyncedFolderPreferencesDialogFragment : DialogFragment(), Injectable {
             binding.btnPositive.isEnabled = false
         } else {
             binding.localFolderContainer.visibility = View.GONE
+            if (MediaFolderType.CUSTOM.id == syncedFolder!!.type.id) {
+                // Show exclude hidden checkbox when {@link MediaFolderType#CUSTOM}
+                binding.settingInstantUploadExcludeHiddenContainer.visibility = View.VISIBLE
+            }
         }
     }
 
@@ -156,7 +164,8 @@ class SyncedFolderPreferencesDialogFragment : DialogFragment(), Injectable {
             binding.settingInstantUploadOnWifiCheckbox,
             binding.settingInstantUploadOnChargingCheckbox,
             binding.settingInstantUploadExistingCheckbox,
-            binding.settingInstantUploadPathUseSubfoldersCheckbox
+            binding.settingInstantUploadPathUseSubfoldersCheckbox,
+            binding.settingInstantUploadExcludeHiddenCheckbox
         )
 
         viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(binding.btnPositive)
@@ -209,6 +218,7 @@ class SyncedFolderPreferencesDialogFragment : DialogFragment(), Injectable {
             binding.settingInstantUploadOnChargingCheckbox.isChecked = it.isChargingOnly
             binding.settingInstantUploadExistingCheckbox.isChecked = it.isExisting
             binding.settingInstantUploadPathUseSubfoldersCheckbox.isChecked = it.isSubfolderByDate
+            binding.settingInstantUploadExcludeHiddenCheckbox.isChecked = it.isExcludeHidden
 
             binding.settingInstantUploadSubfolderRuleSpinner.setSelection(it.subFolderRule.ordinal)
 
@@ -311,6 +321,8 @@ class SyncedFolderPreferencesDialogFragment : DialogFragment(), Injectable {
             binding.settingInstantUploadExistingContainer.alpha = alpha
             binding.settingInstantUploadPathUseSubfoldersContainer.isEnabled = enable
             binding.settingInstantUploadPathUseSubfoldersContainer.alpha = alpha
+            binding.settingInstantUploadExcludeHiddenContainer.isEnabled = enable
+            binding.settingInstantUploadExcludeHiddenContainer.alpha = alpha
             binding.remoteFolderContainer.isEnabled = enable
             binding.remoteFolderContainer.alpha = alpha
             binding.localFolderContainer.isEnabled = enable
@@ -321,6 +333,7 @@ class SyncedFolderPreferencesDialogFragment : DialogFragment(), Injectable {
             binding.settingInstantUploadOnChargingCheckbox.isEnabled = enable
             binding.settingInstantUploadExistingCheckbox.isEnabled = enable
             binding.settingInstantUploadPathUseSubfoldersCheckbox.isEnabled = enable
+            binding.settingInstantUploadExcludeHiddenCheckbox.isEnabled = enable
         }
 
         checkWritableFolder()
@@ -364,6 +377,10 @@ class SyncedFolderPreferencesDialogFragment : DialogFragment(), Injectable {
                     binding.settingInstantUploadSubfolderRuleContainer.visibility = View.GONE
                 }
             }
+            binding.settingInstantUploadExcludeHiddenContainer.setOnClickListener {
+                syncedFolder.isExcludeHidden = !syncedFolder.isExcludeHidden
+                binding.settingInstantUploadExcludeHiddenCheckbox.toggle()
+            }
             binding.settingInstantUploadSubfolderRuleSpinner.onItemSelectedListener =
                 object : AdapterView.OnItemSelectedListener {
                     override fun onItemSelected(adapterView: AdapterView<*>?, view: View, i: Int, l: Long) {

+ 12 - 0
app/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java

@@ -49,6 +49,7 @@ public class SyncedFolderParcelable implements Parcelable {
     private String account;
     private int section;
     private SubFolderRule subFolderRule;
+    private boolean excludeHidden;
 
     public SyncedFolderParcelable(SyncedFolderDisplayItem syncedFolderDisplayItem, int section) {
         id = syncedFolderDisplayItem.getId();
@@ -68,6 +69,7 @@ public class SyncedFolderParcelable implements Parcelable {
         this.section = section;
         hidden = syncedFolderDisplayItem.isHidden();
         subFolderRule = syncedFolderDisplayItem.getSubfolderRule();
+        excludeHidden = syncedFolderDisplayItem.isExcludeHidden();
     }
 
     private SyncedFolderParcelable(Parcel read) {
@@ -87,6 +89,7 @@ public class SyncedFolderParcelable implements Parcelable {
         section = read.readInt();
         hidden = read.readInt() != 0;
         subFolderRule = SubFolderRule.values()[read.readInt()];
+        excludeHidden = read.readInt() != 0;
     }
 
     public SyncedFolderParcelable() {
@@ -111,6 +114,7 @@ public class SyncedFolderParcelable implements Parcelable {
         dest.writeInt(section);
         dest.writeInt(hidden ? 1 : 0);
         dest.writeInt(subFolderRule.ordinal());
+        dest.writeInt(excludeHidden ? 1 : 0);
     }
 
     public static final Creator<SyncedFolderParcelable> CREATOR =
@@ -279,4 +283,12 @@ public class SyncedFolderParcelable implements Parcelable {
         this.section = section;
     }
     public void setSubFolderRule(SubFolderRule subFolderRule) { this.subFolderRule = subFolderRule; }
+
+    public boolean isExcludeHidden() {
+        return excludeHidden;
+    }
+
+    public void setExcludeHidden(boolean excludeHidden) {
+        this.excludeHidden = excludeHidden;
+    }
 }

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

@@ -26,6 +26,11 @@ import android.text.TextUtils;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation;
 
+import org.lukhnos.nnio.file.FileVisitResult;
+import org.lukhnos.nnio.file.FileVisitor;
+import org.lukhnos.nnio.file.Path;
+import org.lukhnos.nnio.file.impl.FileBasedPathImpl;
+
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -77,4 +82,19 @@ public final class FileUtil {
             return null;
         }
     }
+
+    public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor) throws IOException {
+        if (org.lukhnos.nnio.file.Files.isDirectory(start)) {
+            org.lukhnos.nnio.file.FileVisitResult preVisitDirectoryResult = visitor.preVisitDirectory(start, null);
+            if (preVisitDirectoryResult == FileVisitResult.CONTINUE) {
+                for (File child : start.toFile().listFiles()) {
+                    walkFileTree(FileBasedPathImpl.get(child), visitor);
+                }
+            }
+            visitor.postVisitDirectory(start, null);
+        } else {
+            visitor.visitFile(start, new org.lukhnos.nnio.file.attribute.BasicFileAttributes(start.toFile()));
+        }
+        return start;
+    }
 }

+ 19 - 10
app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java

@@ -48,7 +48,6 @@ import com.owncloud.android.db.UploadResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 
 import org.lukhnos.nnio.file.FileVisitResult;
-import org.lukhnos.nnio.file.Files;
 import org.lukhnos.nnio.file.Path;
 import org.lukhnos.nnio.file.Paths;
 import org.lukhnos.nnio.file.SimpleFileVisitor;
@@ -94,10 +93,14 @@ public final class FilesSyncHelper {
                     FilesystemDataProvider filesystemDataProvider = new FilesystemDataProvider(contentResolver);
                     Path path = Paths.get(syncedFolder.getLocalPath());
 
-                    Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
+                    FileUtil.walkFileTree(path, new SimpleFileVisitor<Path>() {
                         @Override
                         public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {
                             File file = path.toFile();
+                            if (syncedFolder.isExcludeHidden() && file.isHidden()) {
+                                // exclude hidden file or folder
+                                return FileVisitResult.CONTINUE;
+                            }
                             if (syncedFolder.isExisting() || attrs.lastModifiedTime().toMillis() >= enabledTimestampMs) {
                                 filesystemDataProvider.storeOrUpdateFileValue(path.toAbsolutePath().toString(),
                                                                               attrs.lastModifiedTime().toMillis(),
@@ -107,6 +110,14 @@ public final class FilesSyncHelper {
                             return FileVisitResult.CONTINUE;
                         }
 
+                        @Override
+                        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
+                            if (syncedFolder.isExcludeHidden() && dir.compareTo(Paths.get(syncedFolder.getLocalPath())) != 0 && dir.toFile().isHidden()) {
+                                return null;
+                            }
+                            return FileVisitResult.CONTINUE;
+                        }
+
                         @Override
                         public FileVisitResult visitFileFailed(Path file, IOException exc) {
                             return FileVisitResult.CONTINUE;
@@ -186,11 +197,10 @@ public final class FilesSyncHelper {
 
         for (OCUpload failedUpload : failedUploads) {
             accountExists = false;
-            if(!failedUpload.isWhileChargingOnly()){
+            if (!failedUpload.isWhileChargingOnly()) {
                 whileChargingOnly = false;
             }
-            if(!failedUpload.isUseWifiOnly())
-            {
+            if (!failedUpload.isUseWifiOnly()) {
                 useWifiOnly = false;
             }
 
@@ -208,22 +218,21 @@ public final class FilesSyncHelper {
         }
 
         failedUploads = uploadsStorageManager.getFailedUploads();
-        if(failedUploads.length == 0)
-        {
+        if (failedUploads.length == 0) {
             //nothing to do
             return;
         }
 
-        if(whileChargingOnly){
+        if (whileChargingOnly) {
             final BatteryStatus batteryStatus = powerManagementService.getBattery();
             final boolean charging = batteryStatus.isCharging() || batteryStatus.isFull();
-            if(!charging){
+            if (!charging) {
                 //all uploads requires charging
                 return;
             }
         }
 
-        if (useWifiOnly && !connectivityService.getConnectivity().isWifi()){
+        if (useWifiOnly && !connectivityService.getConnectivity().isWifi()) {
             //all uploads requires wifi
             return;
         }

+ 51 - 0
app/src/main/res/layout/synced_folders_settings_layout.xml

@@ -305,6 +305,57 @@
                 </LinearLayout>
             </LinearLayout>
 
+            <LinearLayout
+                android:id="@+id/setting_instant_upload_exclude_hidden_container"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:baselineAligned="false">
+
+                <RelativeLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:padding="@dimen/standard_padding">
+
+                    <TextView
+                        android:id="@+id/setting_instant_upload_exclude_hidden_label"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:ellipsize="marquee"
+                        android:maxLines="2"
+                        android:text="@string/prefs_instant_upload_exclude_hidden_title"
+                        android:textAppearance="?attr/textAppearanceListItem" />
+
+                    <TextView
+                        android:id="@+id/setting_instant_upload_exclude_hidden_summary"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_below="@id/setting_instant_upload_exclude_hidden_label"
+                        android:layout_alignStart="@id/setting_instant_upload_exclude_hidden_label"
+                        android:ellipsize="end"
+                        android:maxLines="2"
+                        android:text="@string/prefs_instant_upload_exclude_hidden_summary"
+                        android:textColor="?android:attr/textColorSecondary" />
+                </RelativeLayout>
+
+                <!-- Preference should place its actual preference widget here. -->
+                <LinearLayout
+                    android:id="@+id/setting_instant_upload_exclude_hidden_frame"
+                    android:layout_width="@dimen/synced_folders_control_width"
+                    android:layout_height="match_parent"
+                    android:gravity="center"
+                    android:padding="@dimen/standard_padding">
+
+                    <androidx.appcompat.widget.AppCompatCheckBox
+                        android:id="@+id/setting_instant_upload_exclude_hidden_checkbox"
+                        android:layout_width="32dp"
+                        android:layout_height="wrap_content"
+                        android:background="@null"
+                        android:clickable="false"
+                        android:focusable="false" />
+                </LinearLayout>
+            </LinearLayout>
+
             <LinearLayout
                 android:id="@+id/setting_instant_upload_subfolder_rule_container"
                 android:layout_width="match_parent"

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -368,6 +368,8 @@
     <string name="prefs_synced_folders_remote_path_title">Remote folder</string>
     <string name="prefs_instant_upload_path_use_subfolders_title">Use subfolders</string>
     <string name="prefs_instant_upload_path_use_date_subfolders_summary">Store in subfolders based on date</string>
+    <string name="prefs_instant_upload_exclude_hidden_title">Exclude hidden</string>
+    <string name="prefs_instant_upload_exclude_hidden_summary">Exclude hidden file or folder</string>
 
     <string name="prefs_instant_upload_subfolder_rule_title">Subfolder options</string>
 

+ 2 - 1
app/src/test/java/com/owncloud/android/ui/activity/SyncedFoldersActivityTest.java

@@ -178,6 +178,7 @@ public class SyncedFoldersActivityTest {
                                            2,
                                            MediaFolderType.IMAGE,
                                            false,
-                                           SubFolderRule.YEAR_MONTH);
+                                           SubFolderRule.YEAR_MONTH,
+                                           true);
     }
 }