Browse Source

Merge pull request #358 from nextcloud/syncedFolders

Auto Upload (Android 7+)
Tobias Kaminsky 8 years ago
parent
commit
088ea9f5aa
77 changed files with 3320 additions and 139 deletions
  1. 2 1
      .gitignore
  2. 7 0
      AndroidManifest.xml
  3. BIN
      res/drawable-hdpi/ic_cellphone.png
  4. BIN
      res/drawable-hdpi/ic_cloud_check.png
  5. BIN
      res/drawable-hdpi/ic_cloud_outline.png
  6. BIN
      res/drawable-hdpi/ic_cloud_sync_off.png
  7. BIN
      res/drawable-hdpi/ic_cloud_sync_on.png
  8. BIN
      res/drawable-hdpi/ic_cloud_upload.png
  9. BIN
      res/drawable-hdpi/ic_dots_vertical.png
  10. BIN
      res/drawable-hdpi/ic_uploads.png
  11. BIN
      res/drawable-mdpi/ic_cellphone.png
  12. BIN
      res/drawable-mdpi/ic_cloud_check.png
  13. BIN
      res/drawable-mdpi/ic_cloud_outline.png
  14. BIN
      res/drawable-mdpi/ic_cloud_sync_off.png
  15. BIN
      res/drawable-mdpi/ic_cloud_sync_on.png
  16. BIN
      res/drawable-mdpi/ic_cloud_upload.png
  17. BIN
      res/drawable-mdpi/ic_dots_vertical.png
  18. BIN
      res/drawable-xhdpi/ic_cellphone.png
  19. BIN
      res/drawable-xhdpi/ic_cloud_check.png
  20. BIN
      res/drawable-xhdpi/ic_cloud_outline.png
  21. BIN
      res/drawable-xhdpi/ic_cloud_sync_off.png
  22. BIN
      res/drawable-xhdpi/ic_cloud_sync_on.png
  23. BIN
      res/drawable-xhdpi/ic_cloud_upload.png
  24. BIN
      res/drawable-xhdpi/ic_dots_vertical.png
  25. BIN
      res/drawable-xxhdpi/ic_cellphone.png
  26. BIN
      res/drawable-xxhdpi/ic_cloud_check.png
  27. BIN
      res/drawable-xxhdpi/ic_cloud_outline.png
  28. BIN
      res/drawable-xxhdpi/ic_cloud_sync_off.png
  29. BIN
      res/drawable-xxhdpi/ic_cloud_sync_on.png
  30. BIN
      res/drawable-xxhdpi/ic_cloud_upload.png
  31. BIN
      res/drawable-xxhdpi/ic_dots_vertical.png
  32. BIN
      res/drawable-xxxhdpi/ic_cellphone.png
  33. BIN
      res/drawable-xxxhdpi/ic_cloud_check.png
  34. BIN
      res/drawable-xxxhdpi/ic_cloud_outline.png
  35. BIN
      res/drawable-xxxhdpi/ic_cloud_sync_off.png
  36. BIN
      res/drawable-xxxhdpi/ic_cloud_sync_on.png
  37. BIN
      res/drawable-xxxhdpi/ic_cloud_upload.png
  38. BIN
      res/drawable-xxxhdpi/ic_dots_vertical.png
  39. 2 3
      res/layout/drawer.xml
  40. 2 3
      res/layout/drawer_header.xml
  41. 70 0
      res/layout/folder_sync_item_header.xml
  42. 98 0
      res/layout/folder_sync_layout.xml
  43. 331 0
      res/layout/folder_sync_settings_layout.xml
  44. 77 0
      res/layout/grid_sync_item.xml
  45. 5 0
      res/menu/drawer_menu.xml
  46. 1 0
      res/values-sw360dp/dims.xml
  47. 23 0
      res/values-sw600dp/dims.xml
  48. 3 3
      res/values/attrs.xml
  49. 2 0
      res/values/dims.xml
  50. 11 0
      res/values/strings.xml
  51. 184 0
      src/com/afollestad/sectionedrecyclerview/SectionedRecyclerViewAdapter.java
  52. 37 2
      src/com/owncloud/android/MainApp.java
  53. 41 0
      src/com/owncloud/android/datamodel/MediaFolder.java
  54. 116 0
      src/com/owncloud/android/datamodel/MediaProvider.java
  55. 160 0
      src/com/owncloud/android/datamodel/SyncedFolder.java
  56. 83 0
      src/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java
  57. 266 0
      src/com/owncloud/android/datamodel/SyncedFolderProvider.java
  58. 138 23
      src/com/owncloud/android/datamodel/ThumbnailsCacheManager.java
  59. 2 3
      src/com/owncloud/android/db/PreferenceManager.java
  60. 16 3
      src/com/owncloud/android/db/ProviderMeta.java
  61. 7 1
      src/com/owncloud/android/files/InstantUploadBroadcastReceiver.java
  62. 59 1
      src/com/owncloud/android/providers/FileContentProvider.java
  63. 86 0
      src/com/owncloud/android/services/SyncedFolderJobService.java
  64. 11 13
      src/com/owncloud/android/services/observer/FolderObserver.java
  65. 74 0
      src/com/owncloud/android/services/observer/SyncedFolderObserver.java
  66. 84 0
      src/com/owncloud/android/services/observer/SyncedFolderObserverService.java
  67. 9 0
      src/com/owncloud/android/ui/activity/DrawerActivity.java
  68. 51 0
      src/com/owncloud/android/ui/activity/FileDisplayActivity.java
  69. 413 0
      src/com/owncloud/android/ui/activity/FolderSyncActivity.java
  70. 77 69
      src/com/owncloud/android/ui/activity/Preferences.java
  71. 187 0
      src/com/owncloud/android/ui/adapter/FolderSyncAdapter.java
  72. 44 0
      src/com/owncloud/android/ui/decoration/MediaGridItemDecoration.java
  73. 294 0
      src/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java
  74. 219 0
      src/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java
  75. 21 3
      src/com/owncloud/android/utils/DisplayUtils.java
  76. 4 8
      src/com/owncloud/android/utils/FileStorageUtils.java
  77. 3 3
      src/com/owncloud/android/utils/RecursiveFileObserver.java

+ 2 - 1
.gitignore

@@ -31,4 +31,5 @@ tests/proguard-project.txt
 .gradle
 .idea
 *.iml
-build
+build
+/gradle.properties

+ 7 - 0
AndroidManifest.xml

@@ -77,8 +77,10 @@
         </activity>
         <activity android:name=".ui.activity.ManageAccountsActivity" />
         <activity android:name=".ui.activity.ParticipateActivity" />
+        <activity android:name=".ui.activity.FolderSyncActivity" />
         <activity android:name=".ui.activity.UploadFilesActivity" />
         <activity android:name=".ui.activity.ReceiveExternalFilesActivity"
+                  
                   android:taskAffinity=""
                   android:excludeFromRecents="true"
                   android:theme="@style/Theme.ownCloud.NoActionBar">
@@ -122,6 +124,7 @@
                 android:name="android.accounts.AccountAuthenticator"
                 android:resource="@xml/authenticator" />
         </service>
+        <service android:name=".services.observer.SyncedFolderObserverService"/>
         <service
             android:name=".syncadapter.FileSyncService"
             android:exported="true" >
@@ -133,6 +136,10 @@
                 android:name="android.content.SyncAdapter"
                 android:resource="@xml/syncadapter_files" />
         </service>
+        <service
+            android:name=".services.SyncedFolderJobService"
+            android:permission="android.permission.BIND_JOB_SERVICE"
+            android:exported="true"/>
 
         <provider
             android:name=".providers.FileContentProvider"

BIN
res/drawable-hdpi/ic_cellphone.png


BIN
res/drawable-hdpi/ic_cloud_check.png


BIN
res/drawable-hdpi/ic_cloud_outline.png


BIN
res/drawable-hdpi/ic_cloud_sync_off.png


BIN
res/drawable-hdpi/ic_cloud_sync_on.png


BIN
res/drawable-hdpi/ic_cloud_upload.png


BIN
res/drawable-hdpi/ic_dots_vertical.png


BIN
res/drawable-hdpi/ic_uploads.png


BIN
res/drawable-mdpi/ic_cellphone.png


BIN
res/drawable-mdpi/ic_cloud_check.png


BIN
res/drawable-mdpi/ic_cloud_outline.png


BIN
res/drawable-mdpi/ic_cloud_sync_off.png


BIN
res/drawable-mdpi/ic_cloud_sync_on.png


BIN
res/drawable-mdpi/ic_cloud_upload.png


BIN
res/drawable-mdpi/ic_dots_vertical.png


BIN
res/drawable-xhdpi/ic_cellphone.png


BIN
res/drawable-xhdpi/ic_cloud_check.png


BIN
res/drawable-xhdpi/ic_cloud_outline.png


BIN
res/drawable-xhdpi/ic_cloud_sync_off.png


BIN
res/drawable-xhdpi/ic_cloud_sync_on.png


BIN
res/drawable-xhdpi/ic_cloud_upload.png


BIN
res/drawable-xhdpi/ic_dots_vertical.png


BIN
res/drawable-xxhdpi/ic_cellphone.png


BIN
res/drawable-xxhdpi/ic_cloud_check.png


BIN
res/drawable-xxhdpi/ic_cloud_outline.png


BIN
res/drawable-xxhdpi/ic_cloud_sync_off.png


BIN
res/drawable-xxhdpi/ic_cloud_sync_on.png


BIN
res/drawable-xxhdpi/ic_cloud_upload.png


BIN
res/drawable-xxhdpi/ic_dots_vertical.png


BIN
res/drawable-xxxhdpi/ic_cellphone.png


BIN
res/drawable-xxxhdpi/ic_cloud_check.png


BIN
res/drawable-xxxhdpi/ic_cloud_outline.png


BIN
res/drawable-xxxhdpi/ic_cloud_sync_off.png


BIN
res/drawable-xxxhdpi/ic_cloud_sync_on.png


BIN
res/drawable-xxxhdpi/ic_cloud_upload.png


BIN
res/drawable-xxxhdpi/ic_dots_vertical.png


+ 2 - 3
res/layout/drawer.xml

@@ -2,9 +2,8 @@
 <!--
   Nextcloud Android client application
 
-  Copyright (C) 2015 Andy Scherzinger
-  Copyright (C) 2016 Nextcloud
-  Copyright (C) 2015 ownCloud
+  Copyright (C) 2016 Andy Scherzinger
+  Copyright (C) 2016 Nextcloud.
 
   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE

+ 2 - 3
res/layout/drawer_header.xml

@@ -2,9 +2,8 @@
 <!--
   Nextcloud Android client application
 
-  Copyright (C) 2015 Andy Scherzinger
-  Copyright (C) 2016 Nextcloud
-  Copyright (C) 2015 ownCloud
+  Copyright (C) 2016 Andy Scherzinger
+  Copyright (C) 2016 Nextcloud.
 
   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE

+ 70 - 0
res/layout/folder_sync_item_header.xml

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2016 Andy Scherzinger
+  Copyright (C) 2016 Nextcloud.
+
+  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/>.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingTop="@dimen/alternate_half_padding"
+                android:paddingBottom="@dimen/alternate_half_padding"
+                android:paddingLeft="@dimen/standard_padding">
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignBottom="@+id/buttonBar"
+        android:layout_alignParentLeft="true"
+        android:layout_alignTop="@+id/buttonBar"
+        android:layout_toLeftOf="@+id/buttonBar"
+        android:ellipsize="middle"
+        android:gravity="start|center_vertical"
+        android:text="Header Text"
+        android:textColor="?android:textColorPrimary"
+        android:textStyle="bold"/>
+
+    <LinearLayout
+        android:id="@+id/buttonBar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentRight="true">
+
+        <ImageButton
+            android:id="@+id/syncStatusButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:background="@color/transparent"
+            android:clickable="true"
+            android:padding="@dimen/standard_half_padding"
+            android:src="@drawable/ic_cloud_sync_off"/>
+
+        <ImageButton
+            android:id="@+id/settingsButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:background="@color/transparent"
+            android:clickable="true"
+            android:paddingBottom="@dimen/standard_half_padding"
+            android:paddingLeft="@dimen/standard_half_padding"
+            android:paddingRight="@dimen/standard_padding"
+            android:paddingTop="@dimen/standard_half_padding"
+            android:src="@drawable/ic_dots_vertical"/>
+    </LinearLayout>
+
+</RelativeLayout>

+ 98 - 0
res/layout/folder_sync_layout.xml

@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2016 Andy Scherzinger
+  Copyright (C) 2016 Nextcloud.
+
+  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/>.
+-->
+<android.support.v4.widget.DrawerLayout android:id="@+id/drawer_layout"
+                                        xmlns:android="http://schemas.android.com/apk/res/android"
+                                        android:layout_width="match_parent"
+                                        android:layout_height="match_parent"
+                                        android:clickable="true"
+                                        android:fitsSystemWindows="true">
+
+    <!-- The main content view -->
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <include
+            layout="@layout/toolbar_standard"/>
+
+        <FrameLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+            <android.support.v7.widget.RecyclerView
+                android:id="@android:id/list"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:clipToPadding="false"
+                android:scrollbarStyle="outsideOverlay"
+                android:scrollbars="vertical"
+                android:visibility="visible"
+                android:layout_marginRight="-3dp"
+                android:layout_marginLeft="-3dp"
+                android:layout_marginBottom="-3dp"/>
+
+            <LinearLayout
+                android:id="@android:id/progress"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:orientation="vertical"
+                android:visibility="gone">
+
+                <ProgressBar
+                    android:id="@+id/folderSyncProgressBar"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_horizontal"/>
+
+                <TextView
+                    android:id="@+id/progressText"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_horizontal"
+                    android:layout_margin="@dimen/standard_half_margin"
+                    android:text="@string/folder_sync_loading_folders"
+                    android:textColor="@color/secondaryTextColor"
+                    android:textStyle="italic"/>
+            </LinearLayout>
+
+            <TextView
+                android:id="@android:id/empty"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:layout_margin="@dimen/standard_margin"
+                android:gravity="center"
+                android:textColor="@color/secondaryTextColor"
+                android:text="@string/folder_sync_no_results"
+                android:visibility="gone" />
+        </FrameLayout>
+
+    </LinearLayout>
+
+    <include
+        layout="@layout/drawer"
+        android:layout_width="240dp"
+        android:layout_height="match_parent"
+        android:layout_gravity="start"/>
+
+</android.support.v4.widget.DrawerLayout>

+ 331 - 0
res/layout/folder_sync_settings_layout.xml

@@ -0,0 +1,331 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2016 Andy Scherzinger
+  Copyright (C) 2016 Nextcloud.
+
+  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/>.
+-->
+<LinearLayout android:id="@+id/root"
+              xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:gravity="center"
+              android:orientation="vertical"
+              android:padding="@dimen/standard_padding">
+
+    <LinearLayout
+        android:id="@+id/top_title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:paddingBottom="@dimen/standard_padding"
+            android:paddingTop="@dimen/standard_padding"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/folder_sync_settings_title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/folder_sync_preferences"
+                android:textAppearance="@style/TextAppearance.AppCompat.Title"/>
+
+            <TextView
+                android:id="@+id/folder_sync_settings_local_folder_path"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:ellipsize="middle"
+                android:maxLines="2"
+                android:text="@string/folder_sync_preferences_folder_path"
+                android:textColor="?android:attr/textColorSecondary"/>
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:gravity="end|top"
+            android:orientation="vertical"
+            android:paddingTop="@dimen/standard_padding"
+            android:paddingLeft="@dimen/standard_padding">
+
+            <android.support.v7.widget.SwitchCompat
+                android:id="@+id/sync_enabled"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:background="@null"
+                android:clickable="false"
+                android:focusable="false"/>
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+    <ScrollView
+        android:id="@+id/details_scroll"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <LinearLayout
+                android:id="@+id/remote_folder_container"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:baselineAligned="false"
+                android:gravity="center_vertical"
+                android:minHeight="?android:attr/listPreferredItemHeightSmall"
+                android:orientation="vertical"
+                android:paddingBottom="@dimen/standard_padding"
+                android:paddingTop="@dimen/standard_padding">
+
+                <TextView
+                    android:id="@+id/remote_folder_title"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:maxLines="1"
+                    android:text="@string/prefs_folder_sync_remote_path_title"
+                    android:textAppearance="?attr/textAppearanceListItem"/>
+
+                <TextView
+                    android:id="@+id/remote_folder_summary"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:ellipsize="middle"
+                    android:maxLines="2"
+                    android:text="@string/placeholder_filename"
+                    android:textColor="?android:attr/textColorSecondary"/>
+
+            </LinearLayout>
+
+            <LinearLayout
+                android:id="@+id/setting_instant_upload_on_wifi_container"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:baselineAligned="false"
+                android:clipToPadding="false"
+                android:gravity="center_vertical"
+                android:minHeight="?attr/listPreferredItemHeightSmall">
+
+                <RelativeLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:paddingBottom="@dimen/standard_padding"
+                    android:paddingTop="@dimen/standard_padding">
+
+                    <TextView
+                        android:id="@+id/setting_instant_upload_on_wifi_label"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:ellipsize="marquee"
+                        android:maxLines="1"
+                        android:text="@string/instant_upload_on_wifi"
+                        android:textAppearance="?attr/textAppearanceListItem"/>
+
+                </RelativeLayout>
+
+                <LinearLayout
+                    android:id="@+id/setting_instant_upload_on_wifi_frame"
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent"
+                    android:gravity="end|center_vertical"
+                    android:orientation="vertical"
+                    android:paddingLeft="@dimen/standard_padding">
+
+                    <android.support.v7.widget.AppCompatCheckBox
+                        android:id="@+id/setting_instant_upload_on_wifi_checkbox"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:background="@null"
+                        android:clickable="false"
+                        android:focusable="false"/>
+
+                </LinearLayout>
+
+            </LinearLayout>
+
+            <LinearLayout
+                android:id="@+id/setting_instant_upload_on_charging_container"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:baselineAligned="false"
+                android:clipToPadding="false"
+                android:gravity="center_vertical"
+                android:minHeight="?attr/listPreferredItemHeightSmall">
+
+                <RelativeLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:paddingBottom="@dimen/standard_padding"
+                    android:paddingTop="@dimen/standard_padding">
+
+                    <TextView
+                        android:id="@+id/setting_instant_upload_on_charging_label"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:ellipsize="marquee"
+                        android:maxLines="1"
+                        android:text="@string/instant_upload_on_charging"
+                        android:textAppearance="?attr/textAppearanceListItem"/>
+
+                </RelativeLayout>
+
+                <LinearLayout
+                    android:id="@+id/setting_instant_upload_on_charging_frame"
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent"
+                    android:gravity="end|center_vertical"
+                    android:orientation="vertical"
+                    android:paddingLeft="@dimen/standard_padding">
+
+                    <android.support.v7.widget.AppCompatCheckBox
+                        android:id="@+id/setting_instant_upload_on_charging_checkbox"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:background="@null"
+                        android:clickable="false"
+                        android:focusable="false"/>
+
+                </LinearLayout>
+
+            </LinearLayout>
+
+            <LinearLayout
+                android:id="@+id/setting_instant_upload_path_use_subfolders_container"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:baselineAligned="false"
+                android:clipToPadding="false"
+                android:gravity="center_vertical"
+                android:minHeight="?attr/listPreferredItemHeightSmall">
+
+                <RelativeLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:paddingBottom="@dimen/standard_padding"
+                    android:paddingTop="@dimen/standard_padding">
+
+                    <TextView
+                        android:id="@+id/setting_instant_upload_path_use_subfolders_label"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:ellipsize="marquee"
+                        android:maxLines="1"
+                        android:text="@string/prefs_instant_upload_path_use_subfolders_title"
+                        android:textAppearance="?attr/textAppearanceListItem"/>
+
+                    <TextView
+                        android:id="@+id/setting_instant_upload_path_use_subfolders_summary"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_alignLeft="@id/setting_instant_upload_path_use_subfolders_label"
+                        android:layout_below="@id/setting_instant_upload_path_use_subfolders_label"
+                        android:ellipsize="end"
+                        android:maxLines="2"
+                        android:text="@string/prefs_instant_upload_path_use_subfolders_summary"
+                        android:textColor="?android:attr/textColorSecondary"/>
+
+                </RelativeLayout>
+
+                <!-- Preference should place its actual preference widget here. -->
+                <LinearLayout
+                    android:id="@+id/setting_instant_upload_path_use_subfolders_frame"
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent"
+                    android:gravity="end|center_vertical"
+                    android:orientation="vertical"
+                    android:paddingLeft="@dimen/standard_padding">
+
+                    <android.support.v7.widget.AppCompatCheckBox
+                        android:id="@+id/setting_instant_upload_path_use_subfolders_checkbox"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:background="@null"
+                        android:clickable="false"
+                        android:focusable="false"/>
+
+                </LinearLayout>
+
+            </LinearLayout>
+
+            <LinearLayout
+                android:id="@+id/setting_instant_behaviour_container"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:baselineAligned="false"
+                android:clipToPadding="false"
+                android:gravity="center_vertical"
+                android:minHeight="?attr/listPreferredItemHeightSmall"
+                android:orientation="vertical"
+                android:paddingBottom="@dimen/standard_padding"
+                android:paddingTop="@dimen/standard_padding">
+
+                <TextView
+                    android:id="@+id/setting_instant_behaviour_title"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:ellipsize="marquee"
+                    android:maxLines="1"
+                    android:text="@string/prefs_instant_behaviour_title"
+                    android:textAppearance="?attr/textAppearanceListItem"/>
+
+                <TextView
+                    android:id="@+id/setting_instant_behaviour_summary"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:ellipsize="end"
+                    android:maxLines="1"
+                    android:text="@string/placeholder_filename"
+                    android:textColor="?android:attr/textColorSecondary"/>
+
+            </LinearLayout>
+
+        </LinearLayout>
+
+    </ScrollView>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="right">
+
+        <android.support.v7.widget.AppCompatButton
+            android:id="@+id/cancel"
+            style="@style/Button.Borderless"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/common_cancel"/>
+
+        <android.support.v7.widget.AppCompatButton
+            android:id="@+id/save"
+            style="@style/Button.Borderless"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/common_save"/>
+
+    </LinearLayout>
+
+</LinearLayout>

+ 77 - 0
res/layout/grid_sync_item.xml

@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2016 Andy Scherzinger
+  Copyright (C) 2016 Nextcloud.
+
+  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/>.
+-->
+<LinearLayout android:id="@+id/grid_item_container"
+              xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:layout_gravity="center_horizontal"
+              android:background="@drawable/list_selector"
+              android:gravity="center_horizontal"
+              android:orientation="vertical">
+
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <com.owncloud.android.ui.SquareImageView
+            android:id="@+id/thumbnail"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:scaleType="centerCrop"
+            android:src="@drawable/ic_menu_archive"/>
+
+        <ImageView
+            android:id="@+id/thumbnailDarkener"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:scaleType="centerCrop"
+            android:background="#99000000"/>
+
+        <LinearLayout
+            android:id="@+id/counterLayout"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical|center_horizontal"
+            android:gravity="center_horizontal"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/next"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="+"
+                android:textColor="#bcbcbc"
+                android:textSize="22dp"
+                android:textStyle="bold"/>
+
+            <TextView
+                android:id="@+id/counter"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@android:drawable/checkbox_off_background"
+                android:text="127"
+                android:textColor="#ffffff"
+                android:textSize="22dp"
+                android:textStyle="bold"/>
+        </LinearLayout>
+    </FrameLayout>
+
+</LinearLayout>

+ 5 - 0
res/menu/drawer_menu.xml

@@ -37,6 +37,11 @@
             android:id="@+id/nav_uploads"
             android:icon="@drawable/ic_uploads"
             android:title="@string/drawer_item_uploads_list"/>
+        <item
+            android:orderInCategory="0"
+            android:id="@+id/nav_folder_sync"
+            android:icon="@drawable/ic_cloud_upload"
+            android:title="@string/drawer_folder_sync"/>
     </group>
 
     <!--

+ 1 - 0
res/values-sw360dp/dims.xml

@@ -22,4 +22,5 @@
   <dimen name="nav_drawer_header_avatar">64dp</dimen>
   <!-- avatar radius needs to 1/2 of the avatar dp value -->
   <dimen name="nav_drawer_header_avatar_radius">32dp</dimen>
+  <integer name="media_grid_width">4</integer>
 </resources>

+ 23 - 0
res/values-sw600dp/dims.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2016 Andy Scherzinger
+  Copyright (C) 2016 Nextcloud.
+
+  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/>.
+-->
+<resources>
+  <integer name="media_grid_width">6</integer>
+</resources>

+ 3 - 3
res/values/attrs.xml

@@ -16,9 +16,9 @@
 	</string-array>
 
 	<string-array name="pref_behaviour_entryValues" translatable="false">
-		<item>NOTHING</item>
-		<item>MOVE</item>
-		<item>DELETE</item>
+		<item>LOCAL_BEHAVIOUR_FORGET</item>
+		<item>LOCAL_BEHAVIOUR_MOVE</item>
+		<item>LOCAL_BEHAVIOUR_DELETE</item>
 	</string-array>
 
 	<string-array name="upload_files_behaviour" translatable="false">

+ 2 - 0
res/values/dims.xml

@@ -99,4 +99,6 @@
     <dimen name="upload_list_item_text_size_independent">12dip</dimen>
     <dimen name="upload_list_item_image_size">35dp</dimen>
     <dimen name="uploader_list_item_layout_image_margin">12dp</dimen>
+    <dimen name="media_grid_spacing">2dp</dimen>
+    <integer name="media_grid_width">4</integer>
 </resources>

+ 11 - 0
res/values/strings.xml

@@ -106,6 +106,7 @@
     <string name="common_cancel_sync">Cancel sync</string>
     <string name="common_cancel">Cancel</string>
     <string name="common_back">Back</string>
+    <string name="common_save">Save</string>
     <string name="common_save_exit">Save &amp; exit</string>
     <string name="common_error">Error</string>
     <string name="common_loading">Loading &#8230;</string>
@@ -316,6 +317,8 @@
 
     <string name="error__upload__local_file_not_copied">%1$s could not be copied to %2$s local folder</string>
     <string name="prefs_instant_upload_path_title">Instant upload folder</string>
+    <string name="prefs_folder_sync_local_path_title">Local folder</string>
+    <string name="prefs_folder_sync_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_subfolders_summary">Store in subfolders based on year and month</string>
 
@@ -500,6 +503,7 @@
     <string name="actionbar_search">Search</string>
     <string name="files_drop_not_supported">This is a Nextcloud feature, please update.</string>
     <string name="learn_more">Learn more</string>
+    <string name="drawer_folder_sync">Auto upload</string>
     <string name="drawer_participate">Participate</string>
     <string name="participate_testing_headline">Help us testing</string>
     <string name="participate_testing_bug_text">Found a bug? Something is odd?</string>
@@ -518,6 +522,13 @@
     <string name="participate_contribute_github_text">Contribute as a developer, see &lt;a href="https://github.com/nextcloud/android/blob/master/CONTRIBUTING.md">CONTRIBUTING.md&lt;/a> for details</string>
     <string name="move_to">Move to&#8230;</string>
     <string name="copy_to">Copy to&#8230;</string>
+    <string name="choose_remote_folder">Choose folder&#8230;</string>
+    <string name="folder_sync_loading_folders">Loading folders&#8230;</string>
+    <string name="folder_sync_no_results">No media folders found.</string>
+    <string name="folder_sync_preferences">Auto upload preferences</string>
+    <string name="folder_sync_settings">Settings</string>
+    <string name="folder_sync_new_info">Instant upload has been revamped completely. Please see the main menu and re-configure your auto upload. Sorry for the inconvenience.\n\nEnjoy the new and extended auto upload capabilities!</string>
+    <string name="folder_sync_preferences_folder_path">For %1$s</string>
     <plurals name="items_selected_count">
         <!--
              As a developer, you should always supply "one" and "other"

+ 184 - 0
src/com/afollestad/sectionedrecyclerview/SectionedRecyclerViewAdapter.java

@@ -0,0 +1,184 @@
+/**
+ * Copyright 2016 Aidan Follestad
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+package com.afollestad.sectionedrecyclerview;
+
+import android.support.annotation.IntRange;
+import android.support.annotation.Nullable;
+import android.support.v4.util.ArrayMap;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.StaggeredGridLayoutManager;
+import android.view.ViewGroup;
+
+import java.util.List;
+
+/**
+ * @author Aidan Follestad (afollestad)
+ */
+public abstract class SectionedRecyclerViewAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
+
+    protected final static int VIEW_TYPE_HEADER = -2;
+    protected final static int VIEW_TYPE_ITEM = -1;
+
+    private final ArrayMap<Integer, Integer> mHeaderLocationMap;
+    private GridLayoutManager mLayoutManager;
+    private ArrayMap<Integer, Integer> mSpanMap;
+    private boolean mShowHeadersForEmptySections;
+
+    public SectionedRecyclerViewAdapter() {
+        mHeaderLocationMap = new ArrayMap<>();
+    }
+
+    public abstract int getSectionCount();
+
+    public abstract int getItemCount(int section);
+
+    public abstract void onBindHeaderViewHolder(VH holder, int section);
+
+    public abstract void onBindViewHolder(VH holder, int section, int relativePosition, int absolutePosition);
+
+    public final boolean isHeader(int position) {
+        return mHeaderLocationMap.get(position) != null;
+    }
+
+    /**
+     * Instructs the list view adapter to whether show headers for empty sections or not.
+     *
+     * @param show flag indicating whether headers for empty sections ought to be shown.
+     */
+    public final void shouldShowHeadersForEmptySections(boolean show) {
+        mShowHeadersForEmptySections = show;
+    }
+
+    public final void setLayoutManager(@Nullable GridLayoutManager lm) {
+        mLayoutManager = lm;
+        if (lm == null) return;
+        lm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
+            @Override
+            public int getSpanSize(int position) {
+                if (isHeader(position))
+                    return mLayoutManager.getSpanCount();
+                final int[] sectionAndPos = getSectionIndexAndRelativePosition(position);
+                final int absPos = position - (sectionAndPos[0] + 1);
+                return getRowSpan(mLayoutManager.getSpanCount(),
+                        sectionAndPos[0], sectionAndPos[1], absPos);
+            }
+        });
+    }
+
+    @SuppressWarnings("UnusedParameters")
+    protected int getRowSpan(int fullSpanSize, int section, int relativePosition, int absolutePosition) {
+        return 1;
+    }
+
+    // returns section along with offsetted position
+    private int[] getSectionIndexAndRelativePosition(int itemPosition) {
+        synchronized (mHeaderLocationMap) {
+            Integer lastSectionIndex = -1;
+            for (final Integer sectionIndex : mHeaderLocationMap.keySet()) {
+                if (itemPosition > sectionIndex) {
+                    lastSectionIndex = sectionIndex;
+                } else {
+                    break;
+                }
+            }
+            return new int[]{mHeaderLocationMap.get(lastSectionIndex), itemPosition - lastSectionIndex - 1};
+        }
+    }
+
+    @Override
+    public final int getItemCount() {
+        int count = 0;
+        mHeaderLocationMap.clear();
+        for (int s = 0; s < getSectionCount(); s++) {
+            int itemCount = getItemCount(s);
+            if (mShowHeadersForEmptySections || (itemCount > 0)) {
+                mHeaderLocationMap.put(count, s);
+                count += itemCount + 1;
+            }
+        }
+        return count;
+    }
+
+    /**
+     * @hide
+     * @deprecated
+     */
+    @Override
+    @Deprecated
+    public final int getItemViewType(int position) {
+        if (isHeader(position)) {
+            return getHeaderViewType(mHeaderLocationMap.get(position));
+        } else {
+            final int[] sectionAndPos = getSectionIndexAndRelativePosition(position);
+            return getItemViewType(sectionAndPos[0],
+                    // offset section view positions
+                    sectionAndPos[1],
+                    position - (sectionAndPos[0] + 1));
+        }
+    }
+
+    @SuppressWarnings("UnusedParameters")
+    @IntRange(from = 0, to = Integer.MAX_VALUE)
+    public int getHeaderViewType(int section) {
+        //noinspection ResourceType
+        return VIEW_TYPE_HEADER;
+    }
+
+    @SuppressWarnings("UnusedParameters")
+    @IntRange(from = 0, to = Integer.MAX_VALUE)
+    public int getItemViewType(int section, int relativePosition, int absolutePosition) {
+        //noinspection ResourceType
+        return VIEW_TYPE_ITEM;
+    }
+
+    /**
+     * @hide
+     * @deprecated
+     */
+    @Override
+    @Deprecated
+    public final void onBindViewHolder(VH holder, int position) {
+        StaggeredGridLayoutManager.LayoutParams layoutParams = null;
+        if (holder.itemView.getLayoutParams() instanceof GridLayoutManager.LayoutParams)
+            layoutParams = new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+        else if (holder.itemView.getLayoutParams() instanceof StaggeredGridLayoutManager.LayoutParams)
+            layoutParams = (StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams();
+        if (isHeader(position)) {
+            if (layoutParams != null) layoutParams.setFullSpan(true);
+            onBindHeaderViewHolder(holder, mHeaderLocationMap.get(position));
+        } else {
+            if (layoutParams != null) layoutParams.setFullSpan(false);
+            final int[] sectionAndPos = getSectionIndexAndRelativePosition(position);
+            final int absPos = position - (sectionAndPos[0] + 1);
+            onBindViewHolder(holder, sectionAndPos[0],
+                    // offset section view positions
+                    sectionAndPos[1], absPos);
+        }
+        if (layoutParams != null)
+            holder.itemView.setLayoutParams(layoutParams);
+    }
+
+    /**
+     * @hide
+     * @deprecated
+     */
+    @Deprecated
+    @Override
+    public final void onBindViewHolder(VH holder, int position, List<Object> payloads) {
+        super.onBindViewHolder(holder, position, payloads);
+    }
+}

+ 37 - 2
src/com/owncloud/android/MainApp.java

@@ -22,12 +22,16 @@ package com.owncloud.android;
 
 import android.app.Activity;
 import android.app.Application;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
 import android.content.SharedPreferences;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.Environment;
+import android.os.IBinder;
 import android.preference.PreferenceManager;
 
 import com.owncloud.android.authentication.PassCodeManager;
@@ -35,6 +39,7 @@ import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory.Policy;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.services.observer.SyncedFolderObserverService;
 import com.owncloud.android.ui.activity.Preferences;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -65,8 +70,12 @@ public class MainApp extends Application {
 
     private static boolean mOnlyOnDevice = false;
 
-    @SuppressFBWarnings("ST")
-    public void onCreate(){
+    private static SyncedFolderObserverService mObserverService;
+
+    @SuppressWarnings("unused")
+    private boolean mBound;
+
+    @SuppressFBWarnings("ST")    public void onCreate(){
         super.onCreate();
         MainApp.mContext = getApplicationContext();
 
@@ -98,6 +107,11 @@ public class MainApp extends Application {
             Log_OC.d("Debug", "start logging");
         }
 
+        Log_OC.d("SyncedFolderObserverService", "Start service SyncedFolderObserverService");
+        Intent i = new Intent(this, SyncedFolderObserverService.class);
+        startService(i);
+        bindService(i, syncedFolderObserverServiceConnection, Context.BIND_AUTO_CREATE);
+
         // register global protection with pass code
         registerActivityLifecycleCallbacks( new ActivityLifecycleCallbacks() {
 
@@ -204,6 +218,10 @@ public class MainApp extends Application {
         return mOnlyOnDevice;
     }
 
+    public static SyncedFolderObserverService getSyncedFolderObserverService() {
+        return mObserverService;
+    }
+
     // user agent
     public static String getUserAgent() {
         String appString = getAppContext().getResources().getString(R.string.user_agent);
@@ -225,4 +243,21 @@ public class MainApp extends Application {
 
         return userAgent;
     }
+
+    /** Defines callbacks for service binding, passed to bindService() */
+    private ServiceConnection syncedFolderObserverServiceConnection = new ServiceConnection() {
+
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            SyncedFolderObserverService.SyncedFolderObserverBinder binder = (SyncedFolderObserverService.SyncedFolderObserverBinder) service;
+            mObserverService = binder.getService();
+            mBound = true;
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName arg0) {
+            mBound = false;
+        }
+    };
+
 }

+ 41 - 0
src/com/owncloud/android/datamodel/MediaFolder.java

@@ -0,0 +1,41 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2016 Andy Scherzinger
+ * Copyright (C) 2016 Nextcloud
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.datamodel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Business object representing a media folder with all information that are gathered via media queries.
+ */
+public class MediaFolder {
+    /** name of the folder. */
+    public String folderName;
+
+    /** absolute path of the folder. */
+    public String absolutePath;
+    
+    /** list of file paths of the folder's content */
+    public List<String> filePaths = new ArrayList<>();
+
+    /** total number of files in the media folder. */
+    public long numberOfFiles;
+}

+ 116 - 0
src/com/owncloud/android/datamodel/MediaProvider.java

@@ -0,0 +1,116 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2016 Andy Scherzinger
+ * Copyright (C) 2016 Nextcloud
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.datamodel;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import com.owncloud.android.MainApp;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Media queries to gain access to media lists for the device.
+ */
+public class MediaProvider {
+    private static final String TAG = MediaProvider.class.getSimpleName();
+
+    // fixed query parameters
+    private static final Uri MEDIA_URI = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+    private static final String[] FILE_PROJECTION = new String[]{MediaStore.MediaColumns.DATA};
+    private static final String FILE_SELECTION = MediaStore.Images.Media.BUCKET_ID + "=";
+    private static final String[] FOLDER_PROJECTION = { "Distinct " + MediaStore.Images.Media.BUCKET_ID,
+            MediaStore.Images.Media.BUCKET_DISPLAY_NAME };
+    private static final String FOLDER_SORT_ORDER = MediaStore.Images.Media.BUCKET_DISPLAY_NAME + " ASC";
+
+    /**
+     * Getting All Images Paths.
+     *
+     * @param contentResolver the content resolver
+     * @param itemLimit       the number of media items (usually images) to be returned per media folder.
+     * @return list with media folders
+     */
+    public static List<MediaFolder> getMediaFolders(ContentResolver contentResolver, int itemLimit) {
+        // query media/image folders
+        Cursor cursorFolders = contentResolver.query(MEDIA_URI, FOLDER_PROJECTION, null, null, FOLDER_SORT_ORDER);
+        List<MediaFolder> mediaFolders = new ArrayList<>();
+        String dataPath = MainApp.getStoragePath() + File.separator + MainApp.getDataFolder();
+
+        if (cursorFolders != null) {
+            String folderName;
+            String fileSortOrder = MediaStore.Images.Media.DATE_TAKEN + " DESC LIMIT " + itemLimit;
+            Cursor cursorImages;
+
+            while (cursorFolders.moveToNext()) {
+                String folderId = cursorFolders.getString(cursorFolders.getColumnIndex(MediaStore.Images.Media
+                        .BUCKET_ID));
+
+                MediaFolder mediaFolder = new MediaFolder();
+                folderName = cursorFolders.getString(cursorFolders.getColumnIndex(
+                        MediaStore.Images.Media.BUCKET_DISPLAY_NAME));
+                mediaFolder.folderName = folderName;
+                mediaFolder.filePaths = new ArrayList<>();
+
+                // query images
+                cursorImages = contentResolver.query(MEDIA_URI, FILE_PROJECTION, FILE_SELECTION + folderId, null,
+                        fileSortOrder);
+                Log.d(TAG, "Reading images for " + mediaFolder.folderName);
+
+                if (cursorImages != null) {
+                    String filePath;
+                    while (cursorImages.moveToNext()) {
+                        filePath = cursorImages.getString(cursorImages.getColumnIndexOrThrow(
+                                MediaStore.MediaColumns.DATA));
+                        mediaFolder.filePaths.add(filePath);
+                        mediaFolder.absolutePath = filePath.substring(0, filePath.lastIndexOf(folderName)
+                                + folderName.length());
+                    }
+                    cursorImages.close();
+
+                    // count images
+                    Cursor count = contentResolver.query(
+                            MEDIA_URI,
+                            FILE_PROJECTION,
+                            FILE_SELECTION + folderId,
+                            null,
+                            null);
+
+                    if (count != null) {
+                        mediaFolder.numberOfFiles = count.getCount();
+                        count.close();
+                    }
+                }
+                if (!mediaFolder.absolutePath.startsWith(dataPath)) {
+                    mediaFolders.add(mediaFolder);
+                }
+            }
+            cursorFolders.close();
+        }
+
+        return mediaFolders;
+    }
+}

+ 160 - 0
src/com/owncloud/android/datamodel/SyncedFolder.java

@@ -0,0 +1,160 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Tobias Kaminsky
+ *   Copyright (C) 2016 Tobias Kaminsky
+ *   Copyright (C) 2016 Nextcloud
+ *
+ *   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.datamodel;
+
+/**
+ * Synced folder entity containing all information per synced folder.
+ */
+public class SyncedFolder {
+    public static final long UNPERSISTED_ID = Long.MIN_VALUE;
+    private long id = UNPERSISTED_ID;
+    private String localPath;
+    private String remotePath;
+    private Boolean wifiOnly;
+    private Boolean chargingOnly;
+    private Boolean subfolderByDate;
+    private String account;
+    private Integer uploadAction;
+    private boolean enabled;
+
+    /**
+     * constructor for already persisted entity.
+     *
+     * @param id              the primary key
+     * @param localPath       local path
+     * @param remotePath      remote path
+     * @param wifiOnly        upload on wifi only flag
+     * @param chargingOnly    upload on charging only
+     * @param subfolderByDate create sub-folders by date (month)
+     * @param account         the account owning the synced folder
+     * @param uploadAction    the action to be done after the upload
+     * @param enabled         flag if synced folder config is active
+     */
+    public SyncedFolder(long id, String localPath, String remotePath, Boolean wifiOnly, Boolean chargingOnly,
+                        Boolean subfolderByDate, String account, Integer uploadAction, Boolean enabled) {
+        this.id = id;
+        this.localPath = localPath;
+        this.remotePath = remotePath;
+        this.wifiOnly = wifiOnly;
+        this.chargingOnly = chargingOnly;
+        this.subfolderByDate = subfolderByDate;
+        this.account = account;
+        this.uploadAction = uploadAction;
+        this.enabled = enabled;
+    }
+
+    /**
+     * constructor for new, to be persisted entity.
+     *
+     * @param localPath       local path
+     * @param remotePath      remote path
+     * @param wifiOnly        upload on wifi only flag
+     * @param chargingOnly    upload on charging only
+     * @param subfolderByDate create sub-folders by date (month)
+     * @param account         the account owning the synced folder
+     * @param uploadAction    the action to be done after the upload
+     * @param enabled         flag if synced folder config is active
+     */
+    public SyncedFolder(String localPath, String remotePath, Boolean wifiOnly, Boolean chargingOnly,
+                        Boolean subfolderByDate, String account, Integer uploadAction, Boolean enabled) {
+        this.localPath = localPath;
+        this.remotePath = remotePath;
+        this.wifiOnly = wifiOnly;
+        this.chargingOnly = chargingOnly;
+        this.subfolderByDate = subfolderByDate;
+        this.account = account;
+        this.uploadAction = uploadAction;
+        this.enabled = enabled;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public void setId(long id) {
+        this.id = id;
+    }
+
+    public String getLocalPath() {
+        return localPath;
+    }
+
+    public void setLocalPath(String localPath) {
+        this.localPath = localPath;
+    }
+
+    public String getRemotePath() {
+        return remotePath;
+    }
+
+    public void setRemotePath(String remotePath) {
+        this.remotePath = remotePath;
+    }
+
+    public Boolean getWifiOnly() {
+        return wifiOnly;
+    }
+
+    public void setWifiOnly(Boolean wifiOnly) {
+        this.wifiOnly = wifiOnly;
+    }
+
+    public Boolean getChargingOnly() {
+        return chargingOnly;
+    }
+
+    public void setChargingOnly(Boolean chargingOnly) {
+        this.chargingOnly = chargingOnly;
+    }
+
+    public Boolean getSubfolderByDate() {
+        return subfolderByDate;
+    }
+
+    public void setSubfolderByDate(Boolean subfolderByDate) {
+        this.subfolderByDate = subfolderByDate;
+    }
+
+    public String getAccount() {
+        return account;
+    }
+
+    public void setAccount(String account) {
+        this.account = account;
+    }
+
+    public Integer getUploadAction() {
+        return uploadAction;
+    }
+
+    public void setUploadAction(Integer uploadAction) {
+        this.uploadAction = uploadAction;
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+}

+ 83 - 0
src/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java

@@ -0,0 +1,83 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Andy Scherzinger
+ *   Copyright (C) 2016 Andy Scherzinger
+ *   Copyright (C) 2016 Nextcloud
+ *
+ *   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.datamodel;
+
+import java.util.List;
+
+/**
+ * Display item specialization for synced folder objects to be displayed in a list/grid view adding further
+ * information to be displayed in the UI but not part of the persisted underlying {@link SyncedFolder} object.
+ */
+public class SyncedFolderDisplayItem extends SyncedFolder {
+    private List<String> filePaths;
+    private String folderName;
+    private long numberOfFiles;
+
+    /**
+     * constructor for the display item specialization for a synced folder object.
+     *
+     * @param id              id
+     * @param localPath       local path
+     * @param remotePath      remote path
+     * @param wifiOnly        upload on wifi only flag
+     * @param chargingOnly    upload on charging only
+     * @param subfolderByDate create sub-folders by date (month)
+     * @param account         the account owning the synced folder
+     * @param uploadAction    the action to be done after the upload
+     * @param enabled         flag if synced folder config is active
+     * @param filePaths       the UI info for the file path
+     * @param folderName      the UI info for the folder's name
+     * @param numberOfFiles   the UI info for number of files within the folder
+     */
+    public SyncedFolderDisplayItem(long id, String localPath, String remotePath, Boolean wifiOnly, Boolean chargingOnly,
+                                   Boolean subfolderByDate, String account, Integer uploadAction, Boolean enabled,
+                                   List<String> filePaths, String folderName, long numberOfFiles) {
+        super(id, localPath, remotePath, wifiOnly, chargingOnly, subfolderByDate, account, uploadAction, enabled);
+        this.filePaths = filePaths;
+        this.folderName = folderName;
+        this.numberOfFiles = numberOfFiles;
+    }
+
+    public List<String> getFilePaths() {
+        return filePaths;
+    }
+
+    public void setFilePaths(List<String> filePaths) {
+        this.filePaths = filePaths;
+    }
+
+    public String getFolderName() {
+        return folderName;
+    }
+
+    public void setFolderName(String folderName) {
+        this.folderName = folderName;
+    }
+
+    public long getNumberOfFiles() {
+        return numberOfFiles;
+    }
+
+    public void setNumberOfFiles(long numberOfFiles) {
+        this.numberOfFiles = numberOfFiles;
+    }
+}

+ 266 - 0
src/com/owncloud/android/datamodel/SyncedFolderProvider.java

@@ -0,0 +1,266 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   Copyright (C) 2016 Andy Scherzinger
+ *   Copyright (C) 2016 Nextcloud.
+ *
+ *   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.datamodel;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+
+import com.owncloud.android.MainApp;
+import com.owncloud.android.db.ProviderMeta;
+import com.owncloud.android.lib.common.utils.Log_OC;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Observable;
+
+/**
+ * Database provider for handling the persistence aspects of {@link SyncedFolder}s.
+ */
+public class SyncedFolderProvider extends Observable {
+    static private final String TAG = SyncedFolderProvider.class.getSimpleName();
+
+    private ContentResolver mContentResolver;
+
+    /**
+     * constructor.
+     *
+     * @param contentResolver the ContentResolver to work with.
+     */
+    public SyncedFolderProvider(ContentResolver contentResolver) {
+        if (contentResolver == null) {
+            throw new IllegalArgumentException("Cannot create an instance with a NULL contentResolver");
+        }
+        mContentResolver = contentResolver;
+    }
+
+    /**
+     * Stores an media folder sync object in database.
+     *
+     * @param syncedFolder synced folder to store
+     * @return synced folder id, -1 if the insert process fails.
+     */
+    public long storeFolderSync(SyncedFolder syncedFolder) {
+        Log_OC.v(TAG, "Inserting " + syncedFolder.getLocalPath() + " with enabled=" + syncedFolder.isEnabled());
+
+        ContentValues cv = createContentValuesFromSyncedFolder(syncedFolder);
+
+        Uri result = mContentResolver.insert(ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, cv);
+
+        if (result != null) {
+            notifyFolderSyncObservers(syncedFolder);
+            return Long.parseLong(result.getPathSegments().get(1));
+        } else {
+            Log_OC.e(TAG, "Failed to insert item " + syncedFolder.getLocalPath() + " into folder sync db.");
+            return -1;
+        }
+    }
+
+    /**
+     * get all synced folder entries.
+     *
+     * @return all synced folder entries, empty if none have been found
+     */
+    public List<SyncedFolder> getSyncedFolders() {
+        Cursor cursor = mContentResolver.query(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
+                null,
+                "1=1",
+                null,
+                null
+        );
+
+        if (cursor != null) {
+            List<SyncedFolder> list = new ArrayList<>(cursor.getCount());
+            if (cursor.moveToFirst()) {
+                do {
+                    SyncedFolder syncedFolder = createSyncedFolderFromCursor(cursor);
+                    if (syncedFolder == null) {
+                        Log_OC.e(TAG, "SyncedFolder could not be created from cursor");
+                    } else {
+                        list.add(cursor.getPosition(), syncedFolder);
+                    }
+                } while (cursor.moveToNext());
+
+            }
+            cursor.close();
+            return list;
+        } else {
+            Log_OC.e(TAG, "DB error creating read all cursor for synced folders.");
+        }
+
+        return new ArrayList<>(0);
+    }
+
+    /**
+     * Update upload status of file uniquely referenced by id.
+     *
+     * @param id      folder sync id.
+     * @param enabled new status.
+     * @return the number of rows updated.
+     */
+    public int updateFolderSyncEnabled(long id, Boolean enabled) {
+        Log_OC.v(TAG, "Storing sync folder id" + id + " with enabled=" + enabled);
+
+        int result = 0;
+        Cursor cursor = mContentResolver.query(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
+                null,
+                ProviderMeta.ProviderTableMeta._ID + "=?",
+                new String[]{String.valueOf(id)},
+                null
+        );
+
+        if (cursor != null && cursor.getCount() == 1) {
+            while (cursor.moveToNext()) {
+                // read sync folder object and update
+                SyncedFolder syncedFolder = createSyncedFolderFromCursor(cursor);
+
+                syncedFolder.setEnabled(enabled);
+
+                // update sync folder object in db
+                result = updateSyncFolder(syncedFolder);
+
+                cursor.close();
+            }
+        } else {
+            if (cursor == null) {
+                Log_OC.e(TAG, "Sync folder db cursor for ID=" + id + " in NULL.");
+            } else {
+                Log_OC.e(TAG, cursor.getCount() + " items for id=" + id + " available in sync folder database. " +
+                        "Expected 1. Failed to update sync folder db.");
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * find a synced folder by local path.
+     *
+     * @param localPath the local path of the local folder
+     * @return the synced folder if found, else null
+     */
+    public SyncedFolder findByLocalPath(String localPath) {
+        SyncedFolder result = null;
+        Cursor cursor = mContentResolver.query(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
+                null,
+                ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH + "==" + localPath,
+                null,
+                null
+        );
+
+        if (cursor != null && cursor.getCount() == 1) {
+            result = createSyncedFolderFromCursor(cursor);
+            cursor.close();
+        } else {
+            if (cursor == null) {
+                Log_OC.e(TAG, "Sync folder db cursor for local path=" + localPath + " in NULL.");
+            } else {
+                Log_OC.e(TAG, cursor.getCount() + " items for local path=" + localPath
+                        + " available in sync folder db. Expected 1. Failed to update sync folder db.");
+            }
+        }
+
+        return result;
+
+    }
+
+    /**
+     * update given synced folder.
+     *
+     * @param syncedFolder the synced folder to be updated.
+     * @return the number of rows updated.
+     */
+    public int updateSyncFolder(SyncedFolder syncedFolder) {
+        Log_OC.v(TAG, "Updating " + syncedFolder.getLocalPath() + " with enabled=" + syncedFolder.isEnabled());
+
+        ContentValues cv = createContentValuesFromSyncedFolder(syncedFolder);
+
+        int result = mContentResolver.update(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
+                cv,
+                ProviderMeta.ProviderTableMeta._ID + "=?",
+                new String[]{String.valueOf(syncedFolder.getId())}
+        );
+
+        if (result > 0) {
+            notifyFolderSyncObservers(syncedFolder);
+        }
+
+        return result;
+    }
+
+    /**
+     * maps a cursor into a SyncedFolder object.
+     *
+     * @param cursor the cursor
+     * @return the mapped SyncedFolder, null if cursor is null
+     */
+    private SyncedFolder createSyncedFolderFromCursor(Cursor cursor) {
+        SyncedFolder syncedFolder = null;
+        if (cursor != null) {
+            long id = cursor.getLong(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta._ID));
+            String localPath = cursor.getString(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH));
+            String remotePath = cursor.getString(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH));
+            Boolean wifiOnly = cursor.getInt(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_WIFI_ONLY)) == 1;
+            Boolean chargingOnly = cursor.getInt(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_CHARGING_ONLY)) == 1;
+            Boolean subfolderByDate = cursor.getInt(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_BY_DATE)) == 1;
+            String accountName = cursor.getString(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT));
+            Integer uploadAction = cursor.getInt(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_UPLOAD_ACTION));
+            Boolean enabled = cursor.getInt(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED)) == 1;
+
+            syncedFolder = new SyncedFolder(id, localPath, remotePath, wifiOnly, chargingOnly, subfolderByDate,
+                    accountName, uploadAction, enabled);
+        }
+        return syncedFolder;
+    }
+
+    /**
+     * create ContentValues object based on given SyncedFolder.
+     *
+     * @param syncedFolder the synced folder
+     * @return the corresponding ContentValues object
+     */
+    @NonNull
+    private ContentValues createContentValuesFromSyncedFolder(SyncedFolder syncedFolder) {
+        ContentValues cv = new ContentValues();
+        cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH, syncedFolder.getLocalPath());
+        cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH, syncedFolder.getRemotePath());
+        cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_WIFI_ONLY, syncedFolder.getWifiOnly());
+        cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_CHARGING_ONLY, syncedFolder.getChargingOnly());
+        cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED, syncedFolder.isEnabled());
+        cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_BY_DATE, syncedFolder.getSubfolderByDate());
+        cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT, syncedFolder.getAccount());
+        cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_UPLOAD_ACTION, syncedFolder.getUploadAction());
+        return cv;
+    }
+
+    /**
+     * Inform all observers about data change.
+     */
+    private void notifyFolderSyncObservers(SyncedFolder syncedFolder) {
+        MainApp.getSyncedFolderObserverService().restartObserver(syncedFolder);
+        Log_OC.d(TAG, "notifying folder sync data observers for changed/added: " + syncedFolder.getLocalPath());
+    }
+}

+ 138 - 23
src/com/owncloud/android/datamodel/ThumbnailsCacheManager.java

@@ -123,7 +123,37 @@ public class ThumbnailsCacheManager {
             return null;
         }
     }
-    
+
+    /**
+     * Converts size of file icon from dp to pixel
+     * @return int
+     */
+    private static int getThumbnailDimension(){
+        // Converts dp to pixel
+        Resources r = MainApp.getAppContext().getResources();
+        return Math.round(r.getDimension(R.dimen.file_icon_size_grid));
+    }
+
+    /**
+     * Add thumbnail to cache
+     * @param imageKey: thumb key
+     * @param bitmap:   image for extracting thumbnail
+     * @param path:     image path
+     * @param px:       thumbnail dp
+     * @return Bitmap
+     */
+    private static Bitmap addThumbnailToCache(String imageKey, Bitmap bitmap, String path, int px){
+
+        Bitmap thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px);
+
+        // Rotate image, obeying exif tag
+        thumbnail = BitmapUtils.rotateImage(thumbnail,path);
+
+        // Add thumbnail to cache
+        addBitmapToCache(imageKey, thumbnail);
+
+        return thumbnail;
+    }
     
     public static void addBitmapToCache(String key, Bitmap bitmap) {
         synchronized (mThumbnailsDiskCacheLock) {
@@ -133,7 +163,6 @@ public class ThumbnailsCacheManager {
         }
     }
 
-
     public static Bitmap getBitmapFromDiskCache(String key) {
         synchronized (mThumbnailsDiskCacheLock) {
             // Wait while disk cache is started from background thread
@@ -250,27 +279,6 @@ public class ThumbnailsCacheManager {
             }
         }
 
-        /**
-         * Add thumbnail to cache
-         * @param imageKey: thumb key
-         * @param bitmap:   image for extracting thumbnail
-         * @param path:     image path
-         * @param px:       thumbnail dp
-         * @return Bitmap
-         */
-        private Bitmap addThumbnailToCache(String imageKey, Bitmap bitmap, String path, int px){
-
-            Bitmap thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px);
-
-            // Rotate image, obeying exif tag
-            thumbnail = BitmapUtils.rotateImage(thumbnail,path);
-
-            // Add thumbnail to cache
-            addBitmapToCache(imageKey, thumbnail);
-
-            return thumbnail;
-        }
-
         /**
          * Converts size of file icon from dp to pixel
          * @return int
@@ -391,6 +399,97 @@ public class ThumbnailsCacheManager {
 
     }
 
+    public static class MediaThumbnailGenerationTask extends AsyncTask<Object, Void, Bitmap> {
+        private final WeakReference<ImageView> mImageViewReference;
+        private File mFile;
+        private String mImageKey = null;
+
+        public MediaThumbnailGenerationTask(ImageView imageView) {
+            // Use a WeakReference to ensure the ImageView can be garbage collected
+            mImageViewReference = new WeakReference<>(imageView);
+        }
+
+        @Override
+        protected Bitmap doInBackground(Object... params) {
+            Bitmap thumbnail = null;
+
+            try {
+                if (params[0] instanceof File) {
+                    mFile = (File) params[0];
+                    if (params.length == 2) {
+                        mImageKey = (String) params[1];
+                    }
+
+                    if (MimeTypeUtil.isImage(mFile)) {
+                        thumbnail = doFileInBackground(mFile);
+                    }
+                }
+            } catch (Throwable t) {
+                // the app should never break due to a problem with thumbnails
+                Log_OC.e(TAG, "Generation of thumbnail for " + mFile.getAbsolutePath() + " failed", t);
+                if (t instanceof OutOfMemoryError) {
+                    System.gc();
+                }
+            }
+
+            return thumbnail;
+        }
+
+        protected void onPostExecute(Bitmap bitmap) {
+            String tagId = "";
+            final ImageView imageView = mImageViewReference.get();
+            if (imageView != null) {
+                if (mFile != null) {
+                    tagId = String.valueOf(mFile.hashCode());
+                }
+
+                if (bitmap != null) {
+                    if (tagId.equals(String.valueOf(imageView.getTag()))) {
+                        imageView.setImageBitmap(bitmap);
+                    }
+                } else {
+                    if (mFile != null) {
+                        if (mFile.isDirectory()) {
+                            imageView.setImageResource(R.drawable.ic_menu_archive);
+                        } else {
+                            if (MimeTypeUtil.isVideo(mFile)) {
+                                imageView.setImageBitmap(ThumbnailsCacheManager.mDefaultVideo);
+                            } else {
+                                imageView.setImageResource(MimeTypeUtil.getFileTypeIconId(null, mFile.getName()));
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        private Bitmap doFileInBackground(File file) {
+            final String imageKey;
+
+            if (mImageKey != null) {
+                imageKey = mImageKey;
+            } else {
+                imageKey = String.valueOf(file.hashCode());
+            }
+
+            // Check disk cache in background thread
+            Bitmap thumbnail = getBitmapFromDiskCache(imageKey);
+
+            // Not found in disk cache
+            if (thumbnail == null) {
+
+                int px = getThumbnailDimension();
+
+                Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(file.getAbsolutePath(), px, px);
+
+                if (bitmap != null) {
+                    thumbnail = addThumbnailToCache(imageKey, bitmap, file.getPath(), px);
+                }
+            }
+            return thumbnail;
+        }
+    }
+
     public static class AvatarGenerationTask extends AsyncTask<String, Void, Bitmap> {
         private final WeakReference<AvatarGenerationListener> mAvatarGenerationListener;
         private final Object mCallContext;
@@ -685,6 +784,22 @@ public class ThumbnailsCacheManager {
         }
     }
 
+    public static class AsyncMediaThumbnailDrawable extends BitmapDrawable {
+        private final WeakReference<MediaThumbnailGenerationTask> bitmapWorkerTaskReference;
+
+        public AsyncMediaThumbnailDrawable(
+                Resources res, Bitmap bitmap, MediaThumbnailGenerationTask bitmapWorkerTask
+        ) {
+
+            super(res, bitmap);
+            bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
+        }
+
+        public MediaThumbnailGenerationTask getBitmapWorkerTask() {
+            return bitmapWorkerTaskReference.get();
+        }
+    }
+
     public static class AsyncAvatarDrawable extends BitmapDrawable {
         private final WeakReference<AvatarGenerationTask> avatarWorkerTaskReference;
 

+ 2 - 3
src/com/owncloud/android/db/PreferenceManager.java

@@ -22,7 +22,6 @@ package com.owncloud.android.db;
 import android.content.Context;
 import android.content.SharedPreferences;
 
-import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.utils.FileStorageUtils;
 
 /**
@@ -160,7 +159,7 @@ public abstract class PreferenceManager {
         saveIntPreference(context, AUTO_PREF__UPLOADER_BEHAVIOR, uploaderBehaviour);
     }
 
-    private static void saveBooleanPreference(Context context, String key, boolean value) {
+    public static void saveBooleanPreference(Context context, String key, boolean value) {
         SharedPreferences.Editor appPreferences = getDefaultSharedPreferences(context.getApplicationContext()).edit();
         appPreferences.putBoolean(key, value);
         appPreferences.apply();
@@ -178,7 +177,7 @@ public abstract class PreferenceManager {
         appPreferences.apply();
     }
 
-    private static SharedPreferences getDefaultSharedPreferences(Context context) {
+    public static SharedPreferences getDefaultSharedPreferences(Context context) {
         return android.preference.PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
     }
 }

+ 16 - 3
src/com/owncloud/android/db/ProviderMeta.java

@@ -33,7 +33,7 @@ import com.owncloud.android.MainApp;
 public class ProviderMeta {
 
     public static final String DB_NAME = "filelist";
-    public static final int DB_VERSION = 15;
+    public static final int DB_VERSION = 16;
 
     private ProviderMeta() {
     }
@@ -43,7 +43,10 @@ public class ProviderMeta {
         public static final String OCSHARES_TABLE_NAME = "ocshares";
         public static final String CAPABILITIES_TABLE_NAME = "capabilities";
         public static final String UPLOADS_TABLE_NAME = "list_of_uploads";
+        public static final String SYNCED_FOLDERS_TABLE_NAME = "synced_folders";
+
         private static final String CONTENT_PREFIX = "content://";
+
         public static final Uri CONTENT_URI = Uri.parse(CONTENT_PREFIX
                 + MainApp.getAuthority() + "/");
         public static final Uri CONTENT_URI_FILE = Uri.parse(CONTENT_PREFIX
@@ -56,6 +59,8 @@ public class ProviderMeta {
                 + MainApp.getAuthority() + "/capabilities");
         public static final Uri CONTENT_URI_UPLOADS = Uri.parse(CONTENT_PREFIX
                 + MainApp.getAuthority() + "/uploads");
+        public static final Uri CONTENT_URI_SYNCED_FOLDERS = Uri.parse(CONTENT_PREFIX
+                + MainApp.getAuthority() + "/synced_folders");
 
         public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.owncloud.file";
         public static final String CONTENT_TYPE_ITEM = "vnd.android.cursor.item/vnd.owncloud.file";
@@ -150,8 +155,16 @@ public class ProviderMeta {
         public static final String UPLOADS_UPLOAD_END_TIMESTAMP = "upload_end_timestamp";
         public static final String UPLOADS_LAST_RESULT = "last_result";
         public static final String UPLOADS_CREATED_BY = "created_by";
-
         public static final String UPLOADS_DEFAULT_SORT_ORDER = ProviderTableMeta._ID  + " collate nocase desc";
 
+        // Columns of synced folder table
+        public static final String SYNCED_FOLDER_LOCAL_PATH = "local_path";
+        public static final String SYNCED_FOLDER_REMOTE_PATH = "remote_path";
+        public static final String SYNCED_FOLDER_WIFI_ONLY = "wifi_only";
+        public static final String SYNCED_FOLDER_CHARGING_ONLY = "charging_only";
+        public static final String SYNCED_FOLDER_ENABLED = "enabled";
+        public static final String SYNCED_FOLDER_SUBFOLDER_BY_DATE = "subfolder_by_date";
+        public static final String SYNCED_FOLDER_ACCOUNT = "account";
+        public static final String SYNCED_FOLDER_UPLOAD_ACTION = "upload_option";
     }
-}
+}

+ 7 - 1
src/com/owncloud/android/files/InstantUploadBroadcastReceiver.java

@@ -32,6 +32,7 @@ import android.provider.MediaStore.Images;
 import android.provider.MediaStore.Video;
 import android.support.v4.content.ContextCompat;
 
+import com.owncloud.android.R;
 import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.db.PreferenceManager;
 import com.owncloud.android.files.services.FileUploader;
@@ -131,12 +132,17 @@ public class InstantUploadBroadcastReceiver extends BroadcastReceiver {
         new FileUploader.UploadRequester();
 
         int behaviour = getUploadBehaviour(context);
+        Boolean subfolderByDate = PreferenceManager.instantPictureUploadPathUseSubfolders(context);
+            SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
+        String uploadPathdef = context.getString(R.string.instant_upload_path);
+        String uploadPath = pref.getString("instant_upload_path", uploadPathdef);
+
         FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
         requester.uploadNewFile(
                 context,
                 account,
                 file_path,
-                FileStorageUtils.getInstantUploadFilePath(context, file_name, date_taken),
+                FileStorageUtils.getInstantUploadFilePath(uploadPath, file_name, date_taken, subfolderByDate),
                 behaviour,
                 mime_type,
                 true,           // create parent folder if not existent

+ 59 - 1
src/com/owncloud/android/providers/FileContentProvider.java

@@ -71,6 +71,7 @@ public class FileContentProvider extends ContentProvider {
     private static final int SHARES = 4;
     private static final int CAPABILITIES = 5;
     private static final int UPLOADS = 6;
+    private static final int SYNCED_FOLDERS = 7;
 
     private static final String TAG = FileContentProvider.class.getSimpleName();
 
@@ -194,6 +195,9 @@ public class FileContentProvider extends ContentProvider {
             case UPLOADS:
                 count = db.delete(ProviderTableMeta.UPLOADS_TABLE_NAME, where, whereArgs);
                 break;
+            case SYNCED_FOLDERS:
+                count = db.delete(ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME, where, whereArgs);
+                break;
             default:
                 //Log_OC.e(TAG, "Unknown uri " + uri);
                 throw new IllegalArgumentException("Unknown uri: " + uri.toString());
@@ -303,6 +307,19 @@ public class FileContentProvider extends ContentProvider {
 
                 }
                 return insertedUploadUri;
+
+            case SYNCED_FOLDERS:
+                Uri insertedSyncedFolderUri = null;
+                long syncedFolderId = db.insert(ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME, null, values);
+                if (syncedFolderId > 0) {
+                    insertedSyncedFolderUri =
+                            ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, syncedFolderId);
+                } else {
+                    throw new SQLException("ERROR " + uri);
+
+                }
+                return insertedSyncedFolderUri;
+
             default:
                 throw new IllegalArgumentException("Unknown uri id: " + uri);
         }
@@ -350,6 +367,7 @@ public class FileContentProvider extends ContentProvider {
         mUriMatcher.addURI(authority, "capabilities/#", CAPABILITIES);
         mUriMatcher.addURI(authority, "uploads/", UPLOADS);
         mUriMatcher.addURI(authority, "uploads/#", UPLOADS);
+        mUriMatcher.addURI(authority, "synced_folders", SYNCED_FOLDERS);
 
         return true;
     }
@@ -424,6 +442,13 @@ public class FileContentProvider extends ContentProvider {
                             + uri.getPathSegments().get(1));
                 }
                 break;
+            case SYNCED_FOLDERS:
+                sqlQuery.setTables(ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME);
+                if (uri.getPathSegments().size() > 1) {
+                    sqlQuery.appendWhere(ProviderTableMeta._ID + "="
+                            + uri.getPathSegments().get(1));
+                }
+                break;
             default:
                 throw new IllegalArgumentException("Unknown uri id: " + uri);
         }
@@ -440,6 +465,9 @@ public class FileContentProvider extends ContentProvider {
                 case UPLOADS:
                     order = ProviderTableMeta.UPLOADS_DEFAULT_SORT_ORDER;
                     break;
+                case SYNCED_FOLDERS:
+                    order = ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH;
+                    break;
                 default: // Files
                     order = ProviderTableMeta.FILE_DEFAULT_SORT_ORDER;
                     break;
@@ -497,6 +525,8 @@ public class FileContentProvider extends ContentProvider {
                 );
                 trimSuccessfulUploads(db);
                 return ret;
+            case SYNCED_FOLDERS:
+                return db.update(ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME, values, selection, selectionArgs);
             default:
                 return db.update(
                         ProviderTableMeta.FILE_TABLE_NAME, values, selection, selectionArgs
@@ -550,6 +580,8 @@ public class FileContentProvider extends ContentProvider {
             // Create uploads table
             createUploadsTable(db);
 
+            // Create synced folders table
+            createSyncedFoldersTable(db);
         }
 
         @Override
@@ -794,10 +826,22 @@ public class FileContentProvider extends ContentProvider {
                 }
             }
 
+            if (oldVersion < 16 && newVersion >= 16) {
+                Log_OC.i("SQL", "Entering in the #16 ADD synced folders table");
+                db.beginTransaction();
+                try {
+                    // Create synced folders table
+                    createSyncedFoldersTable(db);
+                    upgraded = true;
+                    db.setTransactionSuccessful();
+                } finally {
+                    db.endTransaction();
+                }
+            }
+
             if (!upgraded) {
                 Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
             }
-
         }
     }
 
@@ -908,6 +952,20 @@ public class FileContentProvider extends ContentProvider {
         */
     }
 
+    private void createSyncedFoldersTable(SQLiteDatabase db){
+        db.execSQL("CREATE TABLE " + ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME + "("
+        + ProviderTableMeta._ID + " INTEGER PRIMARY KEY, "                          // id
+                + ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH  + " TEXT, "           // local path
+                + ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH + " TEXT, "           // remote path
+                + ProviderTableMeta.SYNCED_FOLDER_WIFI_ONLY + " INTEGER, "          // wifi_only
+                + ProviderTableMeta.SYNCED_FOLDER_CHARGING_ONLY + " INTEGER, "      // charging only
+                + ProviderTableMeta.SYNCED_FOLDER_ENABLED + " INTEGER, "            // enabled
+                + ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_BY_DATE + " INTEGER, "  // subfolder by date
+                + ProviderTableMeta.SYNCED_FOLDER_ACCOUNT + "  TEXT, "              // account
+                + ProviderTableMeta.SYNCED_FOLDER_UPLOAD_ACTION + " INTEGER );"     // upload action
+        );
+    }
+
     /**
      * Version 10 of database does not modify its scheme. It coincides with the upgrade of the ownCloud account names
      * structure to include in it the path to the server instance. Updating the account names and path to local files

+ 86 - 0
src/com/owncloud/android/services/SyncedFolderJobService.java

@@ -0,0 +1,86 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Tobias Kaminsky
+ *   Copyright (C) 2016 Tobias Kaminsky
+ *   Copyright (C) 2016 Nextcloud
+ *
+ *   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.services;
+
+import android.accounts.Account;
+import android.annotation.TargetApi;
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.PersistableBundle;
+
+import com.owncloud.android.MainApp;
+import com.owncloud.android.authentication.AccountUtils;
+import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.operations.UploadFileOperation;
+import com.owncloud.android.utils.FileStorageUtils;
+import com.owncloud.android.utils.MimeTypeUtil;
+
+import java.io.File;
+
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+public class SyncedFolderJobService extends JobService {
+    private static final String TAG = "SyncedFolderJobService";
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        return START_REDELIVER_INTENT;
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        Context context = MainApp.getAppContext();
+        PersistableBundle bundle = params.getExtras();
+        String filePath = bundle.getString("filePath");
+        String remoteFolder = bundle.getString("remotePath");
+        Long dateTaken = bundle.getLong("dateTaken");
+        Boolean subfolderByDate = bundle.getInt("subfolderByDate") == 1;
+        Account account = AccountUtils.getOwnCloudAccountByName(context, bundle.getString("account"));
+        Integer uploadBehaviour = bundle.getInt("uploadBehaviour");
+
+        Log_OC.d(TAG, "startJob: " + params.getJobId() + ", filePath: " + filePath);
+
+        File file = new File(filePath);
+        String mimeType = MimeTypeUtil.getBestMimeTypeByFilename(file.getAbsolutePath());
+
+        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
+        requester.uploadNewFile(
+                context,
+                account,
+                filePath,
+                FileStorageUtils.getInstantUploadFilePath(remoteFolder, file.getName(), dateTaken, subfolderByDate),
+                uploadBehaviour,
+                mimeType,
+                true,           // create parent folder if not existent
+                UploadFileOperation.CREATED_AS_INSTANT_PICTURE
+        );
+        return false;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        return false;
+    }
+}

+ 11 - 13
src/com/owncloud/android/services/observer/FolderObserver.java

@@ -20,10 +20,6 @@
 
 package com.owncloud.android.services.observer;
 
-import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-
 import android.accounts.Account;
 import android.content.Context;
 import android.content.Intent;
@@ -37,6 +33,10 @@ import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.operations.SynchronizeFileOperation;
 import com.owncloud.android.ui.activity.ConflictsResolveActivity;
 
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * Observer watching a folder to request the synchronization of kept-in-sync files
  * inside it.
@@ -114,16 +114,16 @@ public class FolderObserver extends FileObserver {
         synchronized(mObservedChildren) {
             if (path != null && path.length() > 0 && mObservedChildren.containsKey(path)) {
                 
-                if (    ((event & FileObserver.MODIFY) != 0) ||
+                if (    (((event & FileObserver.MODIFY) != 0) ||
                         ((event & FileObserver.ATTRIB) != 0) ||
-                        ((event & FileObserver.MOVED_TO) != 0)
-                        && !mObservedChildren.get(path)) {
-                    
-                        mObservedChildren.put(path, true);
+                        ((event & FileObserver.MOVED_TO) != 0)) &&
+                        !mObservedChildren.get(path)) {
+
+                        mObservedChildren.put(path, Boolean.TRUE);
                 }
                 
                 if ((event & FileObserver.CLOSE_WRITE) != 0 && mObservedChildren.get(path)) {
-                    mObservedChildren.put(path, false);
+                    mObservedChildren.put(path, Boolean.FALSE);
                     shouldSynchronize = true;
                 }
             }
@@ -136,7 +136,6 @@ public class FolderObserver extends FileObserver {
                 (path == null || path.length() == 0)) {
             Log_OC.d(TAG, "Stopping the observance on " + mPath);
         }
-        
     }
     
 
@@ -148,7 +147,7 @@ public class FolderObserver extends FileObserver {
     public void startWatching(String fileName) {
         synchronized (mObservedChildren) {
             if (!mObservedChildren.containsKey(fileName)) {
-                mObservedChildren.put(fileName, false);
+                mObservedChildren.put(fileName, Boolean.FALSE);
             }
         }
         
@@ -214,5 +213,4 @@ public class FolderObserver extends FileObserver {
         // or maybe just toast them;
         // or nothing, very strange fails
     }
-
 }

+ 74 - 0
src/com/owncloud/android/services/observer/SyncedFolderObserver.java

@@ -0,0 +1,74 @@
+package com.owncloud.android.services.observer;
+
+import android.annotation.TargetApi;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Build;
+import android.os.FileObserver;
+import android.os.PersistableBundle;
+import android.util.Log;
+
+import com.owncloud.android.MainApp;
+import com.owncloud.android.datamodel.SyncedFolder;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.services.SyncedFolderJobService;
+import com.owncloud.android.utils.RecursiveFileObserver;
+
+import java.io.File;
+import java.util.Date;
+
+class SyncedFolderObserver extends RecursiveFileObserver {
+
+    private Context context;
+
+    public static final String TAG = "SyncedFolderObserver";
+    private SyncedFolder syncedFolder;
+
+
+    public SyncedFolderObserver(SyncedFolder syncedFolder) {
+        super(syncedFolder.getLocalPath(), FileObserver.CREATE + FileObserver.MOVED_TO);
+
+        context = MainApp.getAppContext();
+        this.syncedFolder = syncedFolder;
+        Log_OC.d("SyncedFolderObserver", "Started watching: " + syncedFolder.getLocalPath());
+    }
+
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    @Override
+    public void onEvent(int event, String path) {
+        Log.d(TAG, "Event: " + event + " Path: " + path);
+
+        File temp = new File(path);
+
+        if (!temp.getName().equalsIgnoreCase("null")) {
+            PersistableBundle bundle = new PersistableBundle();
+            // TODO extract
+            bundle.putString("filePath", path);
+            bundle.putString("remotePath", syncedFolder.getRemotePath());
+            bundle.putLong("dateTaken", new Date().getTime());
+            bundle.putString("account", syncedFolder.getAccount());
+            bundle.putInt("uploadBehaviour", syncedFolder.getUploadAction());
+            bundle.putInt("subfolderByDate", syncedFolder.getSubfolderByDate() ? 1 : 0);
+
+            JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+
+            Long date = new Date().getTime();
+            JobInfo job = new JobInfo.Builder(
+                    date.intValue(),
+                    new ComponentName(context, SyncedFolderJobService.class))
+                    .setRequiresCharging(syncedFolder.getChargingOnly())
+                    .setRequiredNetworkType(syncedFolder.getWifiOnly() ? JobInfo.NETWORK_TYPE_UNMETERED : JobInfo.NETWORK_TYPE_ANY)
+                    .setExtras(bundle)
+                    .setPersisted(true)
+                    .build();
+
+            Integer result = js.schedule(job);
+            if (result <= 0) {
+                Log_OC.d(TAG, "Job failed to start: " + result);
+            }
+        }
+    }
+}

+ 84 - 0
src/com/owncloud/android/services/observer/SyncedFolderObserverService.java

@@ -0,0 +1,84 @@
+package com.owncloud.android.services.observer;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import com.owncloud.android.MainApp;
+import com.owncloud.android.datamodel.SyncedFolder;
+import com.owncloud.android.datamodel.SyncedFolderProvider;
+import com.owncloud.android.lib.common.utils.Log_OC;
+
+import java.util.HashMap;
+
+public class SyncedFolderObserverService extends Service {
+    private static final String TAG = "SyncedFolderObserverService";
+    private SyncedFolderProvider mProvider;
+    private HashMap<String, SyncedFolderObserver> syncedFolderMap = new HashMap<>();
+    private final IBinder mBinder = new SyncedFolderObserverBinder();
+
+    @Override
+    public void onCreate() {
+        mProvider = new SyncedFolderProvider(MainApp.getAppContext().getContentResolver());
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        Log_OC.d(TAG, "start");
+        for (SyncedFolder syncedFolder : mProvider.getSyncedFolders()) {
+            if (syncedFolder.isEnabled()) {
+                Log_OC.d(TAG, "start observer: " + syncedFolder.getLocalPath());
+                SyncedFolderObserver observer = new SyncedFolderObserver(syncedFolder);
+                observer.startWatching();
+                syncedFolderMap.put(syncedFolder.getLocalPath(), observer);
+            }
+        }
+
+        return Service.START_NOT_STICKY;
+    }
+
+    @Override
+    public void onDestroy() {
+        for (SyncedFolderObserver observer : syncedFolderMap.values()) {
+            observer.stopWatching();
+            syncedFolderMap.remove(observer);
+        }
+    }
+
+    /**
+     * Restart oberver if it is enabled
+     * If syncedFolder exists already, use it, otherwise create new observer
+     * @param syncedFolder
+     */
+    public void restartObserver(SyncedFolder syncedFolder){
+        if (syncedFolderMap.containsKey(syncedFolder.getLocalPath())) {
+            Log_OC.d(TAG, "stop observer: " + syncedFolder.getLocalPath());
+            syncedFolderMap.get(syncedFolder.getLocalPath()).stopWatching();
+            syncedFolderMap.remove(syncedFolder.getLocalPath());
+        }
+
+        if (syncedFolder.isEnabled()) {
+            Log_OC.d(TAG, "start observer: " + syncedFolder.getLocalPath());
+            if (syncedFolderMap.containsKey(syncedFolder.getLocalPath())) {
+                syncedFolderMap.get(syncedFolder.getLocalPath()).startWatching();
+            } else {
+                SyncedFolderObserver observer = new SyncedFolderObserver(syncedFolder);
+                observer.startWatching();
+                syncedFolderMap.put(syncedFolder.getLocalPath(), observer);
+            }
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent arg0) {
+        return mBinder;
+    }
+
+    public class SyncedFolderObserverBinder extends Binder {
+        public SyncedFolderObserverService getService() {
+            return SyncedFolderObserverService.this;
+        }
+    }
+
+}

+ 9 - 0
src/com/owncloud/android/ui/activity/DrawerActivity.java

@@ -166,6 +166,11 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
             setupDrawerMenu(mNavigationView);
 
             setupQuotaElement();
+
+            // show folder sync menu item only for Android 7+
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+                mNavigationView.getMenu().removeItem(R.id.nav_folder_sync);
+            }
         }
 
         setupDrawerToggle();
@@ -267,6 +272,10 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
                                 uploadListIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                                 startActivity(uploadListIntent);
                                 break;
+                            case R.id.nav_folder_sync:
+                                Intent folderSyncIntent = new Intent(getApplicationContext(),FolderSyncActivity.class);
+                                startActivity(folderSyncIntent);
+                                break;
                             case R.id.nav_settings:
                                 Intent settingsIntent = new Intent(getApplicationContext(), Preferences.class);
                                 startActivity(settingsIntent);

+ 51 - 0
src/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -33,6 +33,7 @@ import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
+import android.content.SharedPreferences;
 import android.content.SyncRequest;
 import android.content.pm.PackageManager;
 import android.content.res.Resources.NotFoundException;
@@ -56,6 +57,7 @@ import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.db.PreferenceManager;
 import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader;
@@ -236,6 +238,55 @@ public class FileDisplayActivity extends HookActivity
         // always AFTER setContentView(...) in onCreate(); to work around bug in its implementation
 
         setBackgroundText();
+
+        upgradeNotificationForInstantUpload();
+    }
+
+    /**
+     * For Android 5+.
+     * Opens a pop up info for the new instant upload and disabled the old instant upload.
+     */
+    private void upgradeNotificationForInstantUpload() {
+        // check for Android 5+ if legacy instant upload is activated --> disable + show info
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
+                (PreferenceManager.instantPictureUploadEnabled(this) ||
+                        PreferenceManager.instantPictureUploadEnabled(this))) {
+
+            // remove legacy shared preferences
+            SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
+            editor.remove("instant_uploading")
+                    .remove("instant_video_uploading")
+                    .remove("instant_upload_path")
+                    .remove("instant_upload_path_use_subfolders")
+                    .remove("instant_upload_on_wifi")
+                    .remove("instant_upload_on_charging")
+                    .remove("instant_video_upload_path")
+                    .remove("instant_video_upload_path_use_subfolders")
+                    .remove("instant_video_upload_on_wifi")
+                    .remove("instant_video_uploading")
+                    .remove("instant_video_upload_on_charging")
+                    .remove("prefs_instant_behaviour").apply();
+
+            // show info pop-up
+            new AlertDialog.Builder(this, R.style.Theme_ownCloud_Dialog)
+                    .setTitle(R.string.drawer_folder_sync)
+                    .setMessage(R.string.folder_sync_new_info)
+                    .setPositiveButton(R.string.drawer_open, new DialogInterface.OnClickListener() {
+                        public void onClick(DialogInterface dialog, int which) {
+                            // show instant upload
+                            Intent folderSyncIntent = new Intent(getApplicationContext(), FolderSyncActivity.class);
+                            dialog.dismiss();
+                            startActivity(folderSyncIntent);
+                        }
+                    })
+                    .setNegativeButton(R.string.drawer_close, new DialogInterface.OnClickListener() {
+                        public void onClick(DialogInterface dialog, int which) {
+                            dialog.dismiss();
+                        }
+                    })
+                    .setIcon(R.drawable.ic_cloud_upload)
+                    .show();
+        }
     }
 
     @Override

+ 413 - 0
src/com/owncloud/android/ui/activity/FolderSyncActivity.java

@@ -0,0 +1,413 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2016 Andy Scherzinger
+ * Copyright (C) 2016 Nextcloud
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.ui.activity;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
+import com.owncloud.android.authentication.AccountUtils;
+import com.owncloud.android.datamodel.MediaFolder;
+import com.owncloud.android.datamodel.MediaProvider;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.datamodel.SyncedFolder;
+import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
+import com.owncloud.android.datamodel.SyncedFolderProvider;
+import com.owncloud.android.ui.adapter.FolderSyncAdapter;
+import com.owncloud.android.ui.decoration.MediaGridItemDecoration;
+import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment;
+import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimerTask;
+
+import static com.owncloud.android.datamodel.SyncedFolderDisplayItem.UNPERSISTED_ID;
+
+/**
+ * Activity displaying all auto-synced folders and/or instant upload media folders.
+ */
+public class FolderSyncActivity extends FileActivity implements FolderSyncAdapter.ClickListener,
+        SyncedFolderPreferencesDialogFragment.OnSyncedFolderPreferenceListener {
+    private static final String TAG = FolderSyncActivity.class.getSimpleName();
+
+    private static final String SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG";
+    public static final String PRIORITIZED_FOLDER = "Camera";
+
+    private RecyclerView mRecyclerView;
+    private FolderSyncAdapter mAdapter;
+    private LinearLayout mProgress;
+    private TextView mEmpty;
+    private SyncedFolderProvider mSyncedFolderProvider;
+    private List<SyncedFolderDisplayItem> syncFolderItems;
+    private SyncedFolderPreferencesDialogFragment mSyncedFolderPreferencesDialogFragment;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.folder_sync_layout);
+
+        // setup toolbar
+        setupToolbar();
+
+        // setup drawer
+        setupDrawer(R.id.nav_folder_sync);
+        getSupportActionBar().setTitle(getString(R.string.drawer_folder_sync));
+
+        setupContent();
+    }
+
+    /**
+     * sets up the UI elements and loads all media/synced folders.
+     */
+    private void setupContent() {
+        mRecyclerView = (RecyclerView) findViewById(android.R.id.list);
+
+        mProgress = (LinearLayout) findViewById(android.R.id.progress);
+        mEmpty = (TextView) findViewById(android.R.id.empty);
+
+        final int gridWidth = getResources().getInteger(R.integer.media_grid_width);
+        mAdapter = new FolderSyncAdapter(this, gridWidth, this);
+        mSyncedFolderProvider = new SyncedFolderProvider(getContentResolver());
+
+        final GridLayoutManager lm = new GridLayoutManager(this, gridWidth);
+        mAdapter.setLayoutManager(lm);
+        int spacing = getResources().getDimensionPixelSize(R.dimen.media_grid_spacing);
+        mRecyclerView.addItemDecoration(new MediaGridItemDecoration(spacing));
+        mRecyclerView.setLayoutManager(lm);
+        mRecyclerView.setAdapter(mAdapter);
+
+        load(gridWidth * 2);
+    }
+
+    /**
+     * loads all media/synced folders, adds them to the recycler view adapter and shows the list.
+     *
+     * @param perFolderMediaItemLimit the amount of media items to be loaded/shown per media folder
+     */
+    private void load(final int perFolderMediaItemLimit) {
+        if (mAdapter.getItemCount() > 0) {
+            return;
+        }
+        setListShown(false);
+        final Handler mHandler = new Handler();
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                final List<MediaFolder> mediaFolders = MediaProvider.getMediaFolders(getContentResolver(),
+                        perFolderMediaItemLimit);
+                syncFolderItems = sortSyncedFolderItems(mergeFolderData(mSyncedFolderProvider.getSyncedFolders(),
+                        mediaFolders));
+
+                mHandler.post(new TimerTask() {
+                    @Override
+                    public void run() {
+                        mAdapter.setSyncFolderItems(syncFolderItems);
+                        setListShown(true);
+                    }
+                });
+            }
+        }).start();
+    }
+
+    /**
+     * merges two lists of {@link SyncedFolder} and {@link MediaFolder} items into one of SyncedFolderItems.
+     *
+     * @param syncedFolders the synced folders
+     * @param mediaFolders  the media folders
+     * @return the merged list of SyncedFolderItems
+     */
+    @NonNull
+    private List<SyncedFolderDisplayItem> mergeFolderData(List<SyncedFolder> syncedFolders,
+                                                          @NonNull List<MediaFolder> mediaFolders) {
+        Map<String, SyncedFolder> syncedFoldersMap = createSyncedFoldersMap(syncedFolders);
+        List<SyncedFolderDisplayItem> result = new ArrayList<>();
+
+        for (MediaFolder mediaFolder : mediaFolders) {
+            if (syncedFoldersMap.containsKey(mediaFolder.absolutePath)) {
+                SyncedFolder syncedFolder = syncedFoldersMap.get(mediaFolder.absolutePath);
+                result.add(createSyncedFolder(syncedFolder, mediaFolder));
+            } else {
+                result.add(createSyncedFolderFromMediaFolder(mediaFolder));
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Sorts list of {@link SyncedFolderDisplayItem}s.
+     *
+     * @param syncFolderItemList list of items to be sorted
+     * @return sorted list of items
+     */
+    public static List<SyncedFolderDisplayItem> sortSyncedFolderItems(List<SyncedFolderDisplayItem>
+                                                                              syncFolderItemList) {
+        Collections.sort(syncFolderItemList, new Comparator<SyncedFolderDisplayItem>() {
+            public int compare(SyncedFolderDisplayItem f1, SyncedFolderDisplayItem f2) {
+                if (f1 == null && f2 == null) {
+                    return 0;
+                } else if (f1 == null) {
+                    return -1;
+                } else if (f2 == null) {
+                    return 1;
+                } else if (f1.isEnabled() && f2.isEnabled()) {
+                    return f1.getFolderName().toLowerCase().compareTo(f2.getFolderName().toLowerCase());
+                } else if (f1.isEnabled()) {
+                    return -1;
+                } else if (f2.isEnabled()) {
+                    return 1;
+                } else if (f1.getFolderName() == null && f2.getFolderName() == null) {
+                    return 0;
+                } else if (f1.getFolderName() == null) {
+                    return -1;
+                } else if (f2.getFolderName() == null) {
+                    return 1;
+                } else if (PRIORITIZED_FOLDER.equals(f1.getFolderName())) {
+                    return -1;
+                } else if (PRIORITIZED_FOLDER.equals(f2.getFolderName())) {
+                    return 1;
+                } else {
+                    return f1.getFolderName().toLowerCase().compareTo(f2.getFolderName().toLowerCase());
+                }
+            }
+        });
+
+        return syncFolderItemList;
+    }
+
+    /**
+     * creates a SyncedFolderDisplayItem merging a {@link SyncedFolder} and a {@link MediaFolder} object instance.
+     *
+     * @param syncedFolder the synced folder object
+     * @param mediaFolder  the media folder object
+     * @return the created SyncedFolderDisplayItem
+     */
+    @NonNull
+    private SyncedFolderDisplayItem createSyncedFolder(@NonNull SyncedFolder syncedFolder, @NonNull MediaFolder mediaFolder) {
+        return new SyncedFolderDisplayItem(
+                syncedFolder.getId(),
+                syncedFolder.getLocalPath(),
+                syncedFolder.getRemotePath(),
+                syncedFolder.getWifiOnly(),
+                syncedFolder.getChargingOnly(),
+                syncedFolder.getSubfolderByDate(),
+                syncedFolder.getAccount(),
+                syncedFolder.getUploadAction(),
+                syncedFolder.isEnabled(),
+                mediaFolder.filePaths,
+                mediaFolder.folderName,
+                mediaFolder.numberOfFiles);
+    }
+
+    /**
+     * creates a {@link SyncedFolderDisplayItem} based on a {@link MediaFolder} object instance.
+     *
+     * @param mediaFolder the media folder object
+     * @return the created SyncedFolderDisplayItem
+     */
+    @NonNull
+    private SyncedFolderDisplayItem createSyncedFolderFromMediaFolder(@NonNull MediaFolder mediaFolder) {
+        return new SyncedFolderDisplayItem(
+                UNPERSISTED_ID,
+                mediaFolder.absolutePath,
+                getString(R.string.instant_upload_path) + "/" + mediaFolder.folderName,
+                true,
+                false,
+                false,
+                AccountUtils.getCurrentOwnCloudAccount(this).name,
+                0,
+                false,
+                mediaFolder.filePaths,
+                mediaFolder.folderName,
+                mediaFolder.numberOfFiles);
+    }
+
+    /**
+     * creates a lookup map for a list of given {@link SyncedFolder}s with their local path as the key.
+     *
+     * @param syncFolders list of {@link SyncedFolder}s
+     * @return the lookup map for {@link SyncedFolder}s
+     */
+    @NonNull
+    private Map<String, SyncedFolder> createSyncedFoldersMap(List<SyncedFolder> syncFolders) {
+        Map<String, SyncedFolder> result = new HashMap<>();
+        if (syncFolders != null) {
+            for (SyncedFolder syncFolder : syncFolders) {
+                result.put(syncFolder.getLocalPath(), syncFolder);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * show/hide recycler view list or the empty message / progress info.
+     *
+     * @param shown flag if list should be shown
+     */
+    private void setListShown(boolean shown) {
+        if (mRecyclerView != null) {
+            mRecyclerView.setVisibility(shown ? View.VISIBLE : View.GONE);
+            mProgress.setVisibility(shown ? View.GONE : View.VISIBLE);
+            mEmpty.setVisibility(shown && mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        boolean result;
+        switch (item.getItemId()) {
+            case android.R.id.home: {
+                if (isDrawerOpen()) {
+                    closeDrawer();
+                } else {
+                    openDrawer();
+                }
+            }
+
+            default:
+                result = super.onOptionsItemSelected(item);
+        }
+        return result;
+    }
+
+    @Override
+    public void restart() {
+        Intent i = new Intent(this, FileDisplayActivity.class);
+        i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        startActivity(i);
+    }
+
+    @Override
+    public void showFiles(boolean onDeviceOnly) {
+        MainApp.showOnlyFilesOnDevice(onDeviceOnly);
+        Intent fileDisplayActivity = new Intent(getApplicationContext(), FileDisplayActivity.class);
+        fileDisplayActivity.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        startActivity(fileDisplayActivity);
+    }
+
+    @Override
+    public void onSyncStatusToggleClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem) {
+        if (syncedFolderDisplayItem.getId() > UNPERSISTED_ID) {
+            mSyncedFolderProvider.updateFolderSyncEnabled(syncedFolderDisplayItem.getId(), syncedFolderDisplayItem.isEnabled());
+        } else {
+            mSyncedFolderProvider.storeFolderSync(syncedFolderDisplayItem);
+        }
+    }
+
+    @Override
+    public void onSyncFolderSettingsClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem) {
+        FragmentManager fm = getSupportFragmentManager();
+        FragmentTransaction ft = fm.beginTransaction();
+        ft.addToBackStack(null);
+
+        mSyncedFolderPreferencesDialogFragment = SyncedFolderPreferencesDialogFragment.newInstance(
+                syncedFolderDisplayItem, section);
+        mSyncedFolderPreferencesDialogFragment.show(ft, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_REMOTE_FOLDER
+                && resultCode == RESULT_OK && mSyncedFolderPreferencesDialogFragment != null) {
+            OCFile chosenFolder = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER);
+            mSyncedFolderPreferencesDialogFragment.setRemoteFolderSummary(chosenFolder.getRemotePath());
+
+        } else {
+            super.onActivityResult(requestCode, resultCode, data);
+        }
+    }
+
+    @Override
+    public void onSaveSyncedFolderPreference(SyncedFolderParcelable syncedFolder) {
+        SyncedFolderDisplayItem item = syncFolderItems.get(syncedFolder.getSection());
+        boolean dirty = item.isEnabled() != syncedFolder.getEnabled();
+        item = updateSyncedFolderItem(item, syncedFolder.getLocalPath(), syncedFolder.getRemotePath(), syncedFolder
+                .getWifiOnly(), syncedFolder.getChargingOnly(), syncedFolder.getSubfolderByDate(), syncedFolder
+                .getUploadAction(), syncedFolder.getEnabled());
+
+        if (syncedFolder.getId() == UNPERSISTED_ID) {
+            // newly set up folder sync config
+            mSyncedFolderProvider.storeFolderSync(item);
+        } else {
+            // existing synced folder setup to be updated
+            mSyncedFolderProvider.updateSyncFolder(item);
+        }
+        mSyncedFolderPreferencesDialogFragment = null;
+
+        if(dirty) {
+            mAdapter.setSyncFolderItem(syncedFolder.getSection(), item);
+        }
+    }
+
+    @Override
+    public void onCancelSyncedFolderPreference() {
+        mSyncedFolderPreferencesDialogFragment = null;
+    }
+
+    /**
+     * update given synced folder with the given values.
+     *
+     * @param item            the synced folder to be updated
+     * @param localPath       the local path
+     * @param remotePath      the remote path
+     * @param wifiOnly        upload on wifi only
+     * @param chargingOnly    upload on charging only
+     * @param subfolderByDate created sub folders
+     * @param uploadAction    upload action
+     * @param enabled         is sync enabled
+     * @return the updated item
+     */
+    private SyncedFolderDisplayItem updateSyncedFolderItem(SyncedFolderDisplayItem item,
+                                                           String localPath,
+                                                           String remotePath,
+                                                           Boolean wifiOnly,
+                                                           Boolean chargingOnly,
+                                                           Boolean subfolderByDate,
+                                                           Integer uploadAction,
+                                                           Boolean enabled) {
+        item.setLocalPath(localPath);
+        item.setRemotePath(remotePath);
+        item.setWifiOnly(wifiOnly);
+        item.setChargingOnly(chargingOnly);
+        item.setSubfolderByDate(subfolderByDate);
+        item.setUploadAction(uploadAction);
+        item.setEnabled(enabled);
+        return item;
+    }
+}

+ 77 - 69
src/com/owncloud/android/ui/activity/Preferences.java

@@ -5,6 +5,7 @@
  *   @author David A. Velasco
  *   Copyright (C) 2011  Bartek Przybylski
  *   Copyright (C) 2016 ownCloud Inc.
+ *   Copyright (C) 2016 Nextcloud
  *
  *   This program is free software: you can redistribute it and/or modify
  *   it under the terms of the GNU General Public License version 2,
@@ -30,6 +31,7 @@ import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Configuration;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.preference.CheckBoxPreference;
@@ -69,8 +71,7 @@ import java.io.IOException;
 /**
  * An Activity that allows the user to change the application's settings.
  *
- * It proxies the necessary calls via {@link android.support.v7.app.AppCompatDelegate} to be used
- * with AppCompat.
+ * It proxies the necessary calls via {@link android.support.v7.app.AppCompatDelegate} to be used with AppCompat.
  */
 public class Preferences extends PreferenceActivity
         implements StorageMigration.StorageMigrationProgressListener {
@@ -214,10 +215,10 @@ public class Preferences extends PreferenceActivity
                 preferenceCategory.removePreference(pCalendarContacts);
             }
         }
-        
+
         boolean helpEnabled = getResources().getBoolean(R.bool.help_enabled);
         Preference pHelp = findPreference("help");
-        if (pHelp != null ) {
+        if (pHelp != null) {
             if (helpEnabled) {
                 pHelp.setOnPreferenceClickListener(new OnPreferenceClickListener() {
                     @Override
@@ -236,8 +237,8 @@ public class Preferences extends PreferenceActivity
             }
         }
 
-       boolean recommendEnabled = getResources().getBoolean(R.bool.recommend_enabled);
-       Preference pRecommend =  findPreference("recommend");
+        boolean recommendEnabled = getResources().getBoolean(R.bool.recommend_enabled);
+        Preference pRecommend = findPreference("recommend");
         if (pRecommend != null) {
             if (recommendEnabled) {
                 pRecommend.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@@ -269,7 +270,7 @@ public class Preferences extends PreferenceActivity
         }
 
         boolean feedbackEnabled = getResources().getBoolean(R.bool.feedback_enabled);
-        Preference pFeedback =  findPreference("feedback");
+        Preference pFeedback = findPreference("feedback");
         if (pFeedback != null) {
             if (feedbackEnabled) {
                 pFeedback.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@@ -277,7 +278,7 @@ public class Preferences extends PreferenceActivity
                     public boolean onPreferenceClick(Preference preference) {
                         String feedbackMail = getString(R.string.mail_feedback);
                         String feedback = getText(R.string.prefs_feedback) + " - android v" + appVersion;
-                        Intent intent = new Intent(Intent.ACTION_SENDTO); 
+                        Intent intent = new Intent(Intent.ACTION_SENDTO);
                         intent.setType("text/plain");
                         intent.putExtra(Intent.EXTRA_SUBJECT, feedback);
 
@@ -294,7 +295,7 @@ public class Preferences extends PreferenceActivity
         }
 
         boolean loggerEnabled = getResources().getBoolean(R.bool.logger_enabled) || BuildConfig.DEBUG;
-        Preference pLogger =  findPreference("logger");
+        Preference pLogger = findPreference("logger");
         if (pLogger != null) {
             if (loggerEnabled) {
                 pLogger.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@@ -312,7 +313,7 @@ public class Preferences extends PreferenceActivity
         }
 
         boolean imprintEnabled = getResources().getBoolean(R.bool.imprint_enabled);
-        Preference pImprint =  findPreference("imprint");
+        Preference pImprint = findPreference("imprint");
         if (pImprint != null) {
             if (imprintEnabled) {
                 pImprint.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@@ -333,7 +334,7 @@ public class Preferences extends PreferenceActivity
             }
         }
 
-        mPrefStoragePath =  (ListPreference) findPreference(PreferenceKeys.STORAGE_PATH);
+        mPrefStoragePath = (ListPreference) findPreference(PreferenceKeys.STORAGE_PATH);
         if (mPrefStoragePath != null) {
             StoragePoint[] storageOptions = DataStorageProvider.getInstance().getAvailableStoragePoints();
             String[] entries = new String[storageOptions.length];
@@ -353,22 +354,26 @@ public class Preferences extends PreferenceActivity
                             return true;
                         }
 
-                        StorageMigration storageMigration = new StorageMigration(Preferences.this, mStoragePath, newPath);
+                    StorageMigration storageMigration = new StorageMigration(Preferences.this, mStoragePath, newPath);
 
-                        storageMigration.setStorageMigrationProgressListener(Preferences.this);
+                    storageMigration.setStorageMigrationProgressListener(Preferences.this);
 
-                        storageMigration.migrate();
+                    storageMigration.migrate();
 
-                        return false;
-                    }
-                });
+                    return false;
+                }
+            });
 
         }
 
-        mPrefInstantUploadPath = findPreference(PreferenceKeys.INSTANT_UPLOAD_PATH);
-        if (mPrefInstantUploadPath != null) {
+        mPrefInstantUploadCategory = (PreferenceCategory) findPreference("instant_uploading_category");
+
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+            // Instant upload via preferences on pre Android Lollipop
+            mPrefInstantUploadPath = findPreference("instant_upload_path");
+            if (mPrefInstantUploadPath != null) {
 
-            mPrefInstantUploadPath.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+                mPrefInstantUploadPath.setOnPreferenceClickListener(new OnPreferenceClickListener() {
                     @Override
                     public boolean onPreferenceClick(Preference preference) {
                         if (!mUploadPath.endsWith(OCFile.PATH_SEPARATOR)) {
@@ -380,33 +385,33 @@ public class Preferences extends PreferenceActivity
                         return true;
                     }
                 });
-        }
+            }
 
         mPrefInstantUploadCategory = (PreferenceCategory) findPreference("instant_uploading_category");
 
-        mPrefInstantUploadUseSubfolders = findPreference("instant_upload_path_use_subfolders");
-        mPrefInstantUploadPathWiFi =  findPreference("instant_upload_on_wifi");
-        mPrefInstantPictureUploadOnlyOnCharging = findPreference("instant_upload_on_charging");
-        mPrefInstantUpload = findPreference("instant_uploading");
+            mPrefInstantUploadUseSubfolders = findPreference("instant_upload_path_use_subfolders");
+            mPrefInstantUploadPathWiFi = findPreference("instant_upload_on_wifi");
+            mPrefInstantPictureUploadOnlyOnCharging = findPreference("instant_upload_on_charging");
+            mPrefInstantUpload = findPreference("instant_uploading");
 
-        toggleInstantPictureOptions(((CheckBoxPreference) mPrefInstantUpload).isChecked());
+            toggleInstantPictureOptions(((CheckBoxPreference) mPrefInstantUpload).isChecked());
 
-        mPrefInstantUpload.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+            mPrefInstantUpload.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
 
-            @Override
-            public boolean onPreferenceChange(Preference preference, Object newValue) {
-                toggleInstantPictureOptions((Boolean) newValue);
-                toggleInstantUploadBehaviour(
-                        ((CheckBoxPreference)mPrefInstantVideoUpload).isChecked(),
-                        (Boolean) newValue);
-                return true;
-            }
-        });
+                @Override
+                public boolean onPreferenceChange(Preference preference, Object newValue) {
+                    toggleInstantPictureOptions((Boolean) newValue);
+                    toggleInstantUploadBehaviour(
+                            ((CheckBoxPreference) mPrefInstantVideoUpload).isChecked(),
+                            (Boolean) newValue);
+                    return true;
+                }
+            });
 
-        mPrefInstantVideoUploadPath =  findPreference(PreferenceKeys.INSTANT_VIDEO_UPLOAD_PATH);
+        mPrefInstantVideoUploadPath = findPreference(PreferenceKeys.INSTANT_VIDEO_UPLOAD_PATH);
         if (mPrefInstantVideoUploadPath != null){
 
-            mPrefInstantVideoUploadPath.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+                mPrefInstantVideoUploadPath.setOnPreferenceClickListener(new OnPreferenceClickListener() {
                     @Override
                     public boolean onPreferenceClick(Preference preference) {
                         if (!mUploadVideoPath.endsWith(OCFile.PATH_SEPARATOR)) {
@@ -419,42 +424,44 @@ public class Preferences extends PreferenceActivity
                         return true;
                     }
                 });
-        }
+            }
 
-        mPrefInstantVideoUploadUseSubfolders = findPreference("instant_video_upload_path_use_subfolders");
-        mPrefInstantVideoUploadPathWiFi =  findPreference("instant_video_upload_on_wifi");
-        mPrefInstantVideoUpload = findPreference("instant_video_uploading");
-        mPrefInstantVideoUploadOnlyOnCharging = findPreference("instant_video_upload_on_charging");
-        toggleInstantVideoOptions(((CheckBoxPreference) mPrefInstantVideoUpload).isChecked());
+            mPrefInstantVideoUploadUseSubfolders = findPreference("instant_video_upload_path_use_subfolders");
+            mPrefInstantVideoUploadPathWiFi = findPreference("instant_video_upload_on_wifi");
+            mPrefInstantVideoUpload = findPreference("instant_video_uploading");
+            mPrefInstantVideoUploadOnlyOnCharging = findPreference("instant_video_upload_on_charging");
+            toggleInstantVideoOptions(((CheckBoxPreference) mPrefInstantVideoUpload).isChecked());
+            mPrefInstantVideoUpload.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+                @Override
+                public boolean onPreferenceChange(Preference preference, Object newValue) {
+                    toggleInstantVideoOptions((Boolean) newValue);
+                    toggleInstantUploadBehaviour(
+                            (Boolean) newValue,
+                            ((CheckBoxPreference) mPrefInstantUpload).isChecked());
+                    return true;
+                }
+            });
 
-        mPrefInstantVideoUpload.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+            mPrefInstantUploadBehaviour = findPreference("prefs_instant_behaviour");
+            toggleInstantUploadBehaviour(
+                    ((CheckBoxPreference) mPrefInstantVideoUpload).isChecked(),
+                    ((CheckBoxPreference) mPrefInstantUpload).isChecked());
 
-            @Override
-            public boolean onPreferenceChange(Preference preference, Object newValue) {
-                toggleInstantVideoOptions((Boolean) newValue);
-                toggleInstantUploadBehaviour(
-                        (Boolean) newValue,
-                        ((CheckBoxPreference) mPrefInstantUpload).isChecked());
-                return true;
-            }
-        });
-
-        mPrefInstantUploadBehaviour = findPreference("prefs_instant_behaviour");
-        toggleInstantUploadBehaviour(
-                ((CheckBoxPreference)mPrefInstantVideoUpload).isChecked(),
-                ((CheckBoxPreference)mPrefInstantUpload).isChecked());
+            loadInstantUploadPath();
+            loadInstantUploadVideoPath();
+        } else {
+            // Instant upload is handled via synced folders on Android Lollipop and up
+            getPreferenceScreen().removePreference(mPrefInstantUploadCategory);
+        }
 
         /* About App */
-       pAboutApp = findPreference("about_app");
-       if (pAboutApp != null) { 
-               pAboutApp.setTitle(String.format(getString(R.string.about_android),
-                       getString(R.string.app_name)));
-               pAboutApp.setSummary(String.format(getString(R.string.about_version), appVersion));
-       }
+        pAboutApp = findPreference("about_app");
+        if (pAboutApp != null) {
+            pAboutApp.setTitle(String.format(getString(R.string.about_android), getString(R.string.app_name)));
+            pAboutApp.setSummary(String.format(getString(R.string.about_version), appVersion));
+        }
 
-       loadInstantUploadPath();
-       loadStoragePath();
-       loadInstantUploadVideoPath();
+        loadStoragePath();
     }
 
     private void launchDavDroidLogin()
@@ -635,7 +642,8 @@ public class Preferences extends PreferenceActivity
                 Toast.makeText(this, R.string.pass_code_removed, Toast.LENGTH_LONG).show();
             }
         } else if (requestCode == ACTION_REQUEST_CODE_DAVDROID_SETUP && resultCode == RESULT_OK) {
-            Toast.makeText(this, R.string.prefs_calendar_contacts_sync_setup_successful, Toast.LENGTH_LONG).show();        }
+            Toast.makeText(this, R.string.prefs_calendar_contacts_sync_setup_successful, Toast.LENGTH_LONG).show();
+        }
     }
 
     public ActionBar getSupportActionBar() {

+ 187 - 0
src/com/owncloud/android/ui/adapter/FolderSyncAdapter.java

@@ -0,0 +1,187 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Andy Scherzinger
+ *   Copyright (C) 2016 Andy Scherzinger
+ *   Copyright (C) 2016 Nextcloud
+ *
+ *   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.ui.adapter;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter;
+import com.owncloud.android.R;
+import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
+import com.owncloud.android.datamodel.ThumbnailsCacheManager;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Adapter to display all auto-synced folders and/or instant upload media folders.
+ */
+public class FolderSyncAdapter extends SectionedRecyclerViewAdapter<FolderSyncAdapter.MainViewHolder> {
+
+    private static final String TAG = FolderSyncAdapter.class.getSimpleName();
+
+    private final Context mContext;
+    private final int mGridWidth;
+    private final int mGridTotal;
+    private final ClickListener mListener;
+    private final List<SyncedFolderDisplayItem> mSyncFolderItems;
+
+    public FolderSyncAdapter(Context context, int gridWidth, ClickListener listener) {
+        mContext = context;
+        mGridWidth = gridWidth;
+        mGridTotal = gridWidth * 2;
+        mListener = listener;
+        mSyncFolderItems = new ArrayList<>();
+    }
+
+    public void setSyncFolderItems(List<SyncedFolderDisplayItem> syncFolderItems) {
+        mSyncFolderItems.clear();
+        mSyncFolderItems.addAll(syncFolderItems);
+        notifyDataSetChanged();
+    }
+
+    public void setSyncFolderItem(int location, SyncedFolderDisplayItem syncFolderItem) {
+        mSyncFolderItems.set(location, syncFolderItem);
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public int getSectionCount() {
+        return mSyncFolderItems.size();
+    }
+
+    @Override
+    public int getItemCount(int section) {
+        return mSyncFolderItems.get(section).getFilePaths().size();
+    }
+
+    @Override
+    public void onBindHeaderViewHolder(final MainViewHolder holder, final int section) {
+        holder.title.setText(mSyncFolderItems.get(section).getFolderName());
+        holder.syncStatusButton.setVisibility(View.VISIBLE);
+        holder.syncStatusButton.setTag(section);
+        holder.syncStatusButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mSyncFolderItems.get(section).setEnabled(!mSyncFolderItems.get(section).isEnabled());
+                setSyncButtonActiveIcon(holder.syncStatusButton, mSyncFolderItems.get(section).isEnabled());
+                mListener.onSyncStatusToggleClick(section, mSyncFolderItems.get(section));
+            }
+        });
+        setSyncButtonActiveIcon(holder.syncStatusButton, mSyncFolderItems.get(section).isEnabled());
+
+        holder.menuButton.setVisibility(View.VISIBLE);
+        holder.menuButton.setTag(section);
+        holder.menuButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mListener.onSyncFolderSettingsClick(section, mSyncFolderItems.get(section));
+            }
+        });
+    }
+
+    @Override
+    public void onBindViewHolder(MainViewHolder holder, int section, int relativePosition, int absolutePosition) {
+
+        File file = new File(mSyncFolderItems.get(section).getFilePaths().get(relativePosition));
+
+        ThumbnailsCacheManager.MediaThumbnailGenerationTask task =
+                new ThumbnailsCacheManager.MediaThumbnailGenerationTask(holder.image);
+
+        ThumbnailsCacheManager.AsyncMediaThumbnailDrawable asyncDrawable =
+                new ThumbnailsCacheManager.AsyncMediaThumbnailDrawable(
+                        mContext.getResources(),
+                        ThumbnailsCacheManager.mDefaultImg,
+                        task
+                );
+        holder.image.setImageDrawable(asyncDrawable);
+
+        task.execute(file);
+
+        // set proper tag
+        holder.image.setTag(file.hashCode());
+
+        holder.itemView.setTag(relativePosition % mGridWidth);
+
+        if (mSyncFolderItems.get(section).getNumberOfFiles() > mGridTotal && relativePosition >= mGridTotal - 1) {
+            holder.counterValue.setText(Long.toString(mSyncFolderItems.get(section).getNumberOfFiles() - mGridTotal));
+            holder.counterBar.setVisibility(View.VISIBLE);
+            holder.thumbnailDarkener.setVisibility(View.VISIBLE);
+        } else {
+            holder.counterBar.setVisibility(View.GONE);
+            holder.thumbnailDarkener.setVisibility(View.GONE);
+        }
+
+        //holder.itemView.setTag(String.format(Locale.getDefault(), "%d:%d:%d", section, relativePos, absolutePos));
+        //holder.itemView.setOnClickListener(this);
+    }
+
+    @Override
+    public MainViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        View v = LayoutInflater.from(parent.getContext()).inflate(
+                viewType == VIEW_TYPE_HEADER ?
+                        R.layout.folder_sync_item_header : R.layout.grid_sync_item, parent, false);
+        return new MainViewHolder(v);
+    }
+
+    public interface ClickListener {
+        void onSyncStatusToggleClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem);
+        void onSyncFolderSettingsClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem);
+    }
+
+    static class MainViewHolder extends RecyclerView.ViewHolder {
+        private final ImageView image;
+        private final TextView title;
+        private final ImageButton menuButton;
+        private final ImageButton syncStatusButton;
+        private final LinearLayout counterBar;
+        private final TextView counterValue;
+        private final ImageView thumbnailDarkener;
+
+        private MainViewHolder(View itemView) {
+            super(itemView);
+            image = (ImageView) itemView.findViewById(R.id.thumbnail);
+            title = (TextView) itemView.findViewById(R.id.title);
+            menuButton = (ImageButton) itemView.findViewById(R.id.settingsButton);
+            syncStatusButton = (ImageButton) itemView.findViewById(R.id.syncStatusButton);
+            counterBar = (LinearLayout) itemView.findViewById(R.id.counterLayout);
+            counterValue = (TextView) itemView.findViewById(R.id.counter);
+            thumbnailDarkener = (ImageView) itemView.findViewById(R.id.thumbnailDarkener);
+        }
+    }
+
+    private void setSyncButtonActiveIcon(ImageButton syncStatusButton, boolean enabled) {
+        if(enabled) {
+            syncStatusButton.setImageResource(R.drawable.ic_cloud_sync_on);
+        } else {
+            syncStatusButton.setImageResource(R.drawable.ic_cloud_sync_off);
+        }
+    }
+}

+ 44 - 0
src/com/owncloud/android/ui/decoration/MediaGridItemDecoration.java

@@ -0,0 +1,44 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   Copyright (C) 2016 Andy Scherzinger
+ *   Copyright (C) 2016 Nextcloud.
+ *
+ *   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.ui.decoration;
+
+import android.graphics.Rect;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ItemDecoration;
+import android.view.View;
+
+/**
+ * Decoration for media grid items.
+ */
+public class MediaGridItemDecoration extends ItemDecoration {
+    private int space;
+
+    public MediaGridItemDecoration(int space) {
+        this.space = space;
+    }
+
+    @Override
+    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+        outRect.right = space;
+        outRect.bottom = space;
+        outRect.left = space;
+        outRect.top = space;
+    }
+}

+ 294 - 0
src/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java

@@ -0,0 +1,294 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2016 Andy Scherzinger
+ * Copyright (C) 2016 Nextcloud
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.ui.dialog;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.SwitchCompat;
+import android.text.style.StyleSpan;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.TextView;
+
+import com.owncloud.android.R;
+import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.ui.activity.FolderPickerActivity;
+import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable;
+import com.owncloud.android.utils.DisplayUtils;
+
+/**
+ * Dialog to show the preferences/configuration of a synced folder allowing the user to change the different parameters.
+ */
+public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
+
+    private final static String TAG = SyncedFolderPreferencesDialogFragment.class.getSimpleName();
+    public static final String SYNCED_FOLDER_PARCELABLE = "SyncedFolderParcelable";
+    public static final int REQUEST_CODE__SELECT_REMOTE_FOLDER = 0;
+
+    private CharSequence[] mUploadBehaviorItemStrings;
+
+    protected View mView = null;
+    private SwitchCompat mEnabledSwitch;
+    private CheckBox mUploadOnWifiCheckbox;
+    private CheckBox mUploadOnChargingCheckbox;
+    private CheckBox mUploadUseSubfoldersCheckbox;
+    private TextView mUploadBehaviorSummary;
+    private TextView mLocalFolderPath;
+    private TextView mRemoteFolderSummary;
+
+    private SyncedFolderParcelable mSyncedFolder;
+
+    public static SyncedFolderPreferencesDialogFragment newInstance(SyncedFolderDisplayItem syncedFolder, int section) {
+        SyncedFolderPreferencesDialogFragment dialogFragment = new SyncedFolderPreferencesDialogFragment();
+
+        if (syncedFolder == null) {
+            throw new IllegalArgumentException("SyncedFolder is mandatory but NULL!");
+        }
+
+        Bundle args = new Bundle();
+        args.putParcelable(SYNCED_FOLDER_PARCELABLE, new SyncedFolderParcelable(syncedFolder, section));
+        dialogFragment.setArguments(args);
+        dialogFragment.setStyle(STYLE_NORMAL,R.style.Theme_ownCloud_Dialog);
+
+        return dialogFragment;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        if (!(activity instanceof OnSyncedFolderPreferenceListener)) {
+            throw new IllegalArgumentException("The host activity must implement "
+                    + OnSyncedFolderPreferenceListener.class.getCanonicalName());
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        // keep the state of the fragment on configuration changes
+        setRetainInstance(true);
+
+        setCancelable(false);
+        mView = null;
+
+        mSyncedFolder = getArguments().getParcelable(SYNCED_FOLDER_PARCELABLE);
+        mUploadBehaviorItemStrings = getResources().getTextArray(R.array.pref_behaviour_entries);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        Log_OC.d(TAG, "onCreateView, savedInstanceState is " + savedInstanceState);
+
+        mView = inflater.inflate(R.layout.folder_sync_settings_layout, container, false);
+
+        setupDialogElements(mView);
+        setupListeners(mView);
+
+        return mView;
+    }
+
+    /**
+     * find all relevant UI elements and set their values.
+     *
+     * @param view the parent view
+     */
+    private void setupDialogElements(View view) {
+        // find/saves UI elements
+        mEnabledSwitch = (SwitchCompat) view.findViewById(R.id.sync_enabled);
+        mLocalFolderPath = (TextView) view.findViewById(R.id.folder_sync_settings_local_folder_path);
+
+        mRemoteFolderSummary = (TextView) view.findViewById(R.id.remote_folder_summary);
+
+        mUploadOnWifiCheckbox = (CheckBox) view.findViewById(R.id.setting_instant_upload_on_wifi_checkbox);
+        mUploadOnChargingCheckbox = (CheckBox) view.findViewById(R.id.setting_instant_upload_on_charging_checkbox);
+        mUploadUseSubfoldersCheckbox = (CheckBox) view.findViewById(R.id
+                .setting_instant_upload_path_use_subfolders_checkbox);
+
+        mUploadBehaviorSummary = (TextView) view.findViewById(R.id.setting_instant_behaviour_summary);
+
+        // Set values
+        setEnabled(mSyncedFolder.getEnabled());
+        mLocalFolderPath.setText(
+                DisplayUtils.createTextWithSpan(
+                        String.format(
+                                getString(R.string.folder_sync_preferences_folder_path),
+                                mSyncedFolder.getLocalPath()),
+                        mSyncedFolder.getFolderName(),
+                        new StyleSpan(Typeface.BOLD)));
+
+        mRemoteFolderSummary.setText(mSyncedFolder.getRemotePath());
+
+        mUploadOnWifiCheckbox.setChecked(mSyncedFolder.getWifiOnly());
+        mUploadOnChargingCheckbox.setChecked(mSyncedFolder.getChargingOnly());
+        mUploadUseSubfoldersCheckbox.setChecked(mSyncedFolder.getSubfolderByDate());
+
+        mUploadBehaviorSummary.setText(mUploadBehaviorItemStrings[mSyncedFolder.getUploadActionInteger()]);
+    }
+
+    /**
+     * set correct icon/flag.
+     *
+     * @param enabled if enabled or disabled
+     */
+    private void setEnabled(boolean enabled) {
+        mSyncedFolder.setEnabled(enabled);
+        mEnabledSwitch.setChecked(enabled);
+    }
+
+    /**
+     * set (new) remote path on activity result of the folder picker activity. The result gets originally propagated
+     * to the underlying activity since the picker is an activity and the result can't get passed to the dialog
+     * fragment directly.
+     *
+     * @param path the remote path to be set
+     */
+    public void setRemoteFolderSummary(String path) {
+        mSyncedFolder.setRemotePath(path);
+        mRemoteFolderSummary.setText(path);
+    }
+
+    /**
+     * setup all listeners.
+     *
+     * @param view the parent view
+     */
+    private void setupListeners(View view) {
+        view.findViewById(R.id.save).setOnClickListener(new OnSyncedFolderSaveClickListener());
+        view.findViewById(R.id.cancel).setOnClickListener(new OnSyncedFolderCancelClickListener());
+
+        view.findViewById(R.id.setting_instant_upload_on_wifi_container).setOnClickListener(
+                new OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        mSyncedFolder.setWifiOnly(!mSyncedFolder.getWifiOnly());
+                        mUploadOnWifiCheckbox.toggle();
+                    }
+                });
+
+        view.findViewById(R.id.setting_instant_upload_on_charging_container).setOnClickListener(
+                new OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        mSyncedFolder.setChargingOnly(!mSyncedFolder.getChargingOnly());
+                        mUploadOnChargingCheckbox.toggle();
+                    }
+                });
+
+        view.findViewById(R.id.setting_instant_upload_path_use_subfolders_container).setOnClickListener(
+                new OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        mSyncedFolder.setSubfolderByDate(!mSyncedFolder.getSubfolderByDate());
+                        mUploadUseSubfoldersCheckbox.toggle();
+                    }
+                });
+
+        view.findViewById(R.id.remote_folder_container).setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Intent action = new Intent(getActivity(), FolderPickerActivity.class);
+                action.putExtra(
+                        FolderPickerActivity.EXTRA_ACTION, getResources().getText(R.string.choose_remote_folder));
+                getActivity().startActivityForResult(action, REQUEST_CODE__SELECT_REMOTE_FOLDER);
+            }
+        });
+
+        view.findViewById(R.id.sync_enabled).setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                setEnabled(!mSyncedFolder.getEnabled());
+            }
+        });
+
+        view.findViewById(R.id.setting_instant_behaviour_container).setOnClickListener(
+                new OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+                        builder.setTitle(R.string.prefs_instant_behaviour_dialogTitle)
+                                .setSingleChoiceItems(getResources().getTextArray(R.array.pref_behaviour_entries),
+                                        mSyncedFolder.getUploadActionInteger(),
+                                        new
+                                                DialogInterface.OnClickListener() {
+                                                    public void onClick(DialogInterface dialog, int which) {
+                                                        mSyncedFolder.setUploadAction(
+                                                        getResources().getTextArray(
+                                                                R.array.pref_behaviour_entryValues)[which].toString());
+                                                        mUploadBehaviorSummary.setText(SyncedFolderPreferencesDialogFragment
+                                                                .this.mUploadBehaviorItemStrings[which]);
+                                                        dialog.dismiss();
+                                                    }
+                                                });
+                        builder.create().show();
+                    }
+                });
+    }
+
+    @Override
+    @NonNull
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final Dialog dialog = super.onCreateDialog(savedInstanceState);
+        dialog.setTitle(null);
+        return dialog;
+    }
+
+    @Override
+    public void onDestroyView() {
+        Log_OC.d(TAG, "destroy SyncedFolderPreferencesDialogFragment view");
+        if (getDialog() != null && getRetainInstance()) {
+            getDialog().setDismissMessage(null);
+        }
+        super.onDestroyView();
+    }
+
+    private class OnSyncedFolderSaveClickListener implements OnClickListener {
+        @Override
+        public void onClick(View v) {
+            dismiss();
+            ((OnSyncedFolderPreferenceListener) getActivity()).onSaveSyncedFolderPreference(mSyncedFolder);
+        }
+    }
+
+    private class OnSyncedFolderCancelClickListener implements OnClickListener {
+        @Override
+        public void onClick(View v) {
+            dismiss();
+            ((OnSyncedFolderPreferenceListener) getActivity()).onCancelSyncedFolderPreference();
+        }
+    }
+
+    public interface OnSyncedFolderPreferenceListener {
+        void onSaveSyncedFolderPreference(SyncedFolderParcelable syncedFolder);
+
+        void onCancelSyncedFolderPreference();
+    }
+}

+ 219 - 0
src/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java

@@ -0,0 +1,219 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2016 Andy Scherzinger
+ * Copyright (C) 2016 Nextcloud
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.ui.dialog.parcel;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
+import com.owncloud.android.files.services.FileUploader;
+
+/**
+ * Parcelable for {@link SyncedFolderDisplayItem} objects to transport them from/to dialog fragments.
+ */
+public class SyncedFolderParcelable implements Parcelable {
+    private String mFolderName;
+    private String mLocalPath;
+    private String mRemotePath;
+    private Boolean mWifiOnly = false;
+    private Boolean mChargingOnly = false;
+    private Boolean mEnabled = false;
+    private Boolean mSubfolderByDate = false;
+    private Integer mUploadAction;
+    private long mId;
+    private String mAccount;
+    private int mSection;
+
+    public SyncedFolderParcelable() {
+    }
+
+    public SyncedFolderParcelable(SyncedFolderDisplayItem syncedFolderDisplayItem, int section) {
+        mId = syncedFolderDisplayItem.getId();
+        mFolderName = syncedFolderDisplayItem.getFolderName();
+        mLocalPath = syncedFolderDisplayItem.getLocalPath();
+        mRemotePath = syncedFolderDisplayItem.getRemotePath();
+        mWifiOnly = syncedFolderDisplayItem.getWifiOnly();
+        mChargingOnly = syncedFolderDisplayItem.getChargingOnly();
+        mEnabled = syncedFolderDisplayItem.isEnabled();
+        mSubfolderByDate = syncedFolderDisplayItem.getSubfolderByDate();
+        mAccount = syncedFolderDisplayItem.getAccount();
+        mUploadAction = syncedFolderDisplayItem.getUploadAction();
+        mSection = section;
+    }
+
+    private SyncedFolderParcelable(Parcel read) {
+        mId = read.readLong();
+        mFolderName = read.readString();
+        mLocalPath = read.readString();
+        mRemotePath = read.readString();
+        mWifiOnly = read.readInt()!= 0;
+        mChargingOnly = read.readInt() != 0;
+        mEnabled = read.readInt() != 0;
+        mSubfolderByDate = read.readInt() != 0;
+        mAccount = read.readString();
+        mUploadAction = read.readInt();
+        mSection = read.readInt();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeLong(mId);
+        dest.writeString(mFolderName);
+        dest.writeString(mLocalPath);
+        dest.writeString(mRemotePath);
+        dest.writeInt(mWifiOnly ? 1 : 0);
+        dest.writeInt(mChargingOnly ? 1 : 0);
+        dest.writeInt(mEnabled ? 1 : 0);
+        dest.writeInt(mSubfolderByDate ? 1 : 0);
+        dest.writeString(mAccount);
+        dest.writeInt(mUploadAction);
+        dest.writeInt(mSection);
+    }
+
+    public static final Creator<SyncedFolderParcelable> CREATOR =
+            new Creator<SyncedFolderParcelable>() {
+
+                @Override
+                public SyncedFolderParcelable createFromParcel(Parcel source) {
+                    return new SyncedFolderParcelable(source);
+                }
+
+                @Override
+                public SyncedFolderParcelable[] newArray(int size) {
+                    return new SyncedFolderParcelable[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public String getFolderName() {
+        return mFolderName;
+    }
+
+    public void setFolderName(String mFolderName) {
+        this.mFolderName = mFolderName;
+    }
+
+    public String getLocalPath() {
+        return mLocalPath;
+    }
+
+    public void setLocalPath(String mLocalPath) {
+        this.mLocalPath = mLocalPath;
+    }
+
+    public String getRemotePath() {
+        return mRemotePath;
+    }
+
+    public void setRemotePath(String mRemotePath) {
+        this.mRemotePath = mRemotePath;
+    }
+
+    public Boolean getWifiOnly() {
+        return mWifiOnly;
+    }
+
+    public void setWifiOnly(Boolean mWifiOnly) {
+        this.mWifiOnly = mWifiOnly;
+    }
+
+    public Boolean getChargingOnly() {
+        return mChargingOnly;
+    }
+
+    public void setChargingOnly(Boolean mChargingOnly) {
+        this.mChargingOnly = mChargingOnly;
+    }
+
+    public Boolean getEnabled() {
+        return mEnabled;
+    }
+
+    public void setEnabled(boolean mEnabled) {
+        this.mEnabled = mEnabled;
+    }
+
+    public Boolean getSubfolderByDate() {
+        return mSubfolderByDate;
+    }
+
+    public void setSubfolderByDate(Boolean mSubfolderByDate) {
+        this.mSubfolderByDate = mSubfolderByDate;
+    }
+
+    public Integer getUploadAction() {
+        return mUploadAction;
+    }
+
+    public Integer getUploadActionInteger() {
+        switch (mUploadAction) {
+            case FileUploader.LOCAL_BEHAVIOUR_FORGET:
+                return 0;
+            case FileUploader.LOCAL_BEHAVIOUR_MOVE:
+                return 1;
+            case FileUploader.LOCAL_BEHAVIOUR_DELETE:
+                return 2;
+        }
+        return 0;
+    }
+
+    public void setUploadAction(String mUploadAction) {
+        switch (mUploadAction) {
+            case "LOCAL_BEHAVIOUR_FORGET":
+                this.mUploadAction = FileUploader.LOCAL_BEHAVIOUR_FORGET;
+                break;
+            case "LOCAL_BEHAVIOUR_MOVE":
+                this.mUploadAction = FileUploader.LOCAL_BEHAVIOUR_MOVE;
+                break;
+            case "LOCAL_BEHAVIOUR_DELETE":
+                this.mUploadAction = FileUploader.LOCAL_BEHAVIOUR_DELETE;
+                break;
+        }
+    }
+
+    public long getId() {
+        return mId;
+    }
+
+    public void setId(long mId) {
+        this.mId = mId;
+    }
+
+    public String getAccount() {
+        return mAccount;
+    }
+
+    public void setAccount(String mAccount) {
+        this.mAccount = mAccount;
+    }
+
+    public int getSection() {
+        return mSection;
+    }
+
+    public void setSection(int mSection) {
+        this.mSection = mSection;
+    }
+}

+ 21 - 3
src/com/owncloud/android/utils/DisplayUtils.java

@@ -38,7 +38,10 @@ import android.support.annotation.ColorInt;
 import android.support.design.widget.Snackbar;
 import android.support.v4.app.FragmentActivity;
 import android.support.v4.content.ContextCompat;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
 import android.text.format.DateUtils;
+import android.text.style.StyleSpan;
 import android.view.View;
 import android.widget.ProgressBar;
 import android.widget.SeekBar;
@@ -68,9 +71,9 @@ public class DisplayUtils {
 
     private static final String[] sizeSuffixes = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
     private static final int[] sizeScales = { 0, 0, 1, 1, 1, 2, 2, 2, 2 };
-    public static final int RELATIVE_THRESHOLD_WARNING = 90;
-    public static final int RELATIVE_THRESHOLD_CRITICAL = 95;
-    public static final String MIME_TYPE_UNKNOWN = "Unknown type";
+    private static final int RELATIVE_THRESHOLD_WARNING = 90;
+    private static final int RELATIVE_THRESHOLD_CRITICAL = 95;
+    private static final String MIME_TYPE_UNKNOWN = "Unknown type";
 
     private static Map<String, String> mimeType2HumanReadable;
 
@@ -355,6 +358,21 @@ public class DisplayUtils {
         }
     }
 
+    /**
+     * styling of given spanText within a given text.
+     *
+     * @param text     the non styled complete text
+     * @param spanText the to be styled text
+     * @param style    the style to be applied
+     */
+    public static SpannableStringBuilder createTextWithSpan(String text, String spanText, StyleSpan style) {
+        SpannableStringBuilder sb = new SpannableStringBuilder(text);
+        int start = text.lastIndexOf(spanText);
+        int end = start + spanText.length();
+        sb.setSpan(style, start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+        return sb;
+    }
+
     /**
      * Sets the color of the progressbar to {@code color} within the given toolbar.
      *

+ 4 - 8
src/com/owncloud/android/utils/FileStorageUtils.java

@@ -142,21 +142,17 @@ public class FileStorageUtils {
     /**
      * Returns the InstantUploadFilePath on the owncloud instance
      *
-     * @param context
      * @param fileName
      * @param dateTaken: Time in milliseconds since 1970 when the picture was taken.
      * @return
      */
-    public static String getInstantUploadFilePath(Context context, String fileName, long dateTaken) {
-        SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
-        String uploadPathdef = context.getString(R.string.instant_upload_path);
-        String uploadPath = pref.getString("instant_upload_path", uploadPathdef);
+    public static String getInstantUploadFilePath(String remotePath, String fileName, long dateTaken,
+                                                  Boolean subfolderByDate) {
         String subPath = "";
-        if (com.owncloud.android.db.PreferenceManager.instantPictureUploadPathUseSubfolders(context)) {
+        if (subfolderByDate) {
            subPath = getSubpathFromDate(dateTaken);
         }
-        return uploadPath + OCFile.PATH_SEPARATOR + subPath
-                + (fileName == null ? "" : fileName);
+        return remotePath + OCFile.PATH_SEPARATOR + subPath + (fileName == null ? "" : fileName);
     }
 
     /**

+ 3 - 3
src/com/owncloud/android/utils/RecursiveFileObserver.java

@@ -20,13 +20,13 @@
 
 package com.owncloud.android.utils;
 
+import android.os.FileObserver;
+
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Stack;
 
-import android.os.FileObserver;
-
 public class RecursiveFileObserver extends FileObserver {
 
     private final List<SingleFileObserver> mObservers =  new ArrayList<>();
@@ -38,7 +38,7 @@ public class RecursiveFileObserver extends FileObserver {
         this(path, ALL_EVENTS);
     }
     
-    RecursiveFileObserver(String path, int mask) {
+    public RecursiveFileObserver(String path, int mask) {
         super(path, mask);
         mPath = path;
         mMask = mask;