Browse Source

Merge pull request #797 from nextcloud/mediaStoreAutoUpload

Add further media types to auto upload
Andy Scherzinger 7 years ago
parent
commit
bfe2b71210
100 changed files with 2799 additions and 2477 deletions
  1. 49 51
      build.gradle
  2. 1 0
      drawable_resources/ic_folder_star.svg
  3. 1 0
      gradle.properties
  4. 2 2
      gradle/wrapper/gradle-wrapper.properties
  5. 1 0
      lint.xml
  6. 1 1
      scripts/lint/lint-results.txt
  7. 2 2
      src/gplay/java/com/owncloud/android/utils/PushUtils.java
  8. 8 34
      src/main/AndroidManifest.xml
  9. 0 1
      src/main/java/com/afollestad/sectionedrecyclerview/SectionedRecyclerViewAdapter.java
  10. 143 38
      src/main/java/com/owncloud/android/MainApp.java
  11. 3 1
      src/main/java/com/owncloud/android/authentication/PassCodeManager.java
  12. 20 45
      src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.java
  13. 0 2
      src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  14. 105 0
      src/main/java/com/owncloud/android/datamodel/FileSystemDataSet.java
  15. 186 0
      src/main/java/com/owncloud/android/datamodel/FilesystemDataProvider.java
  16. 6 3
      src/main/java/com/owncloud/android/datamodel/MediaFolder.java
  17. 53 0
      src/main/java/com/owncloud/android/datamodel/MediaFolderType.java
  18. 126 30
      src/main/java/com/owncloud/android/datamodel/MediaProvider.java
  19. 26 3
      src/main/java/com/owncloud/android/datamodel/SyncedFolder.java
  20. 6 5
      src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java
  21. 36 42
      src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java
  22. 41 6
      src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java
  23. 127 143
      src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java
  24. 45 10
      src/main/java/com/owncloud/android/db/OCUpload.java
  25. 30 8
      src/main/java/com/owncloud/android/db/PreferenceManager.java
  26. 17 1
      src/main/java/com/owncloud/android/db/ProviderMeta.java
  27. 6 1
      src/main/java/com/owncloud/android/db/UploadResult.java
  28. 0 223
      src/main/java/com/owncloud/android/files/InstantUploadBroadcastReceiver.java
  29. 0 223
      src/main/java/com/owncloud/android/files/services/ConnectivityActionReceiver.java
  30. 123 50
      src/main/java/com/owncloud/android/files/services/FileUploader.java
  31. 2 2
      src/main/java/com/owncloud/android/jobs/AccountRemovalJob.java
  32. 6 3
      src/main/java/com/owncloud/android/jobs/ContactsBackupJob.java
  33. 1 1
      src/main/java/com/owncloud/android/jobs/ContactsImportJob.java
  34. 153 0
      src/main/java/com/owncloud/android/jobs/FilesSyncJob.java
  35. 3 3
      src/main/java/com/owncloud/android/jobs/NCJobCreator.java
  36. 69 0
      src/main/java/com/owncloud/android/jobs/NContentObserverJob.java
  37. 2 7
      src/main/java/com/owncloud/android/operations/MoveFileOperation.java
  38. 4 7
      src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java
  39. 0 13
      src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java
  40. 126 86
      src/main/java/com/owncloud/android/operations/UploadFileOperation.java
  41. 1 1
      src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java
  42. 92 11
      src/main/java/com/owncloud/android/providers/FileContentProvider.java
  43. 0 237
      src/main/java/com/owncloud/android/services/AdvancedFileAlterationListener.java
  44. 0 81
      src/main/java/com/owncloud/android/services/AutoUploadJob.java
  45. 1 1
      src/main/java/com/owncloud/android/services/OperationsService.java
  46. 0 402
      src/main/java/com/owncloud/android/services/observer/AdvancedFileAlterationObserver.java
  47. 0 180
      src/main/java/com/owncloud/android/services/observer/SyncedFolderObserverService.java
  48. 1 1
      src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java
  49. 4 10
      src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
  50. 2 3
      src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java
  51. 1 1
      src/main/java/com/owncloud/android/ui/activity/FileActivity.java
  52. 11 10
      src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  53. 1 2
      src/main/java/com/owncloud/android/ui/activity/FingerprintActivity.java
  54. 1 0
      src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.java
  55. 1 3
      src/main/java/com/owncloud/android/ui/activity/LogHistoryActivity.java
  56. 3 4
      src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java
  57. 54 150
      src/main/java/com/owncloud/android/ui/activity/Preferences.java
  58. 3 1
      src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java
  59. 4 16
      src/main/java/com/owncloud/android/ui/activity/ShareActivity.java
  60. 208 67
      src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java
  61. 59 21
      src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java
  62. 38 19
      src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java
  63. 2 8
      src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java
  64. 51 83
      src/main/java/com/owncloud/android/ui/adapter/ExpandableUploadListAdapter.java
  65. 19 4
      src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java
  66. 67 40
      src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.java
  67. 3 1
      src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java
  68. 4 2
      src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.java
  69. 2 4
      src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.java
  70. 125 25
      src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java
  71. 13 0
      src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java
  72. 10 15
      src/main/java/com/owncloud/android/ui/events/InitiateSyncedFolder.java
  73. 1 1
      src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java
  74. 40 7
      src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java
  75. 1 1
      src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactListFragment.java
  76. 2 2
      src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactsBackupFragment.java
  77. 1 1
      src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java
  78. 3 1
      src/main/java/com/owncloud/android/ui/helpers/UriUploader.java
  79. 2 4
      src/main/java/com/owncloud/android/ui/preview/PreviewTextFragment.java
  80. 22 5
      src/main/java/com/owncloud/android/utils/BitmapUtils.java
  81. 5 0
      src/main/java/com/owncloud/android/utils/DisplayUtils.java
  82. 329 0
      src/main/java/com/owncloud/android/utils/FilesSyncHelper.java
  83. 3 3
      src/main/java/com/owncloud/android/utils/MimeTypeUtil.java
  84. 74 0
      src/main/java/com/owncloud/android/utils/ReceiversHelper.java
  85. 2 1
      src/main/java/com/owncloud/android/utils/ThemeUtils.java
  86. 3 1
      src/main/java/third_parties/in/srain/cube/GridViewWithHeaderAndFooter.java
  87. BIN
      src/main/res/drawable-hdpi/common_error.png
  88. 0 0
      src/main/res/drawable-hdpi/folder.png
  89. BIN
      src/main/res/drawable-hdpi/ic_action_cancel_sync.png
  90. BIN
      src/main/res/drawable-hdpi/ic_action_cancel_white.png
  91. BIN
      src/main/res/drawable-hdpi/ic_action_copy.png
  92. BIN
      src/main/res/drawable-hdpi/ic_action_delete_white.png
  93. BIN
      src/main/res/drawable-hdpi/ic_action_download.png
  94. BIN
      src/main/res/drawable-hdpi/ic_action_move.png
  95. BIN
      src/main/res/drawable-hdpi/ic_action_set_available_offline.png
  96. BIN
      src/main/res/drawable-hdpi/ic_action_set_available_offline_white.png
  97. BIN
      src/main/res/drawable-hdpi/ic_action_share.png
  98. BIN
      src/main/res/drawable-hdpi/ic_action_unset_available_offline.png
  99. BIN
      src/main/res/drawable-hdpi/ic_action_unset_available_offline_white.png
  100. BIN
      src/main/res/drawable-hdpi/ic_cellphone.png

+ 49 - 51
build.gradle

@@ -11,9 +11,10 @@ buildscript {
         maven {
             url 'https://oss.sonatype.org/content/repositories/snapshots/'
         }
+        google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:2.3.2'
+        classpath 'com.android.tools.build:gradle:3.0.0-alpha9'
         classpath 'com.google.gms:google-services:3.0.0'
     }
 }
@@ -29,7 +30,7 @@ configurations.all {
 }
 
 ext {
-    supportLibraryVersion = '25.0.0'
+    supportLibraryVersion = '25.2.0'
     googleLibraryVersion = '10.2.4'
 
     travisBuild = System.getenv("TRAVIS") == "true"
@@ -44,6 +45,7 @@ repositories {
     maven {
         url 'https://oss.sonatype.org/content/repositories/snapshots/'
     }
+    google()
 
     flatDir {
         dirs 'libs'
@@ -63,7 +65,7 @@ android {
     }
 
     compileSdkVersion 25
-    buildToolsVersion '25.0.0'
+    buildToolsVersion '25.0.2'
 
     defaultConfig {
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@@ -78,14 +80,18 @@ android {
         // adapt structure from Eclipse to Gradle/Android Studio expectations;
         // see http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Configuring-the-Structure
 
+        flavorDimensions "default"
+
         productFlavors {
             // used for f-droid
             generic {
                 applicationId 'com.nextcloud.client'
+                dimension "default"
             }
 
             gplay {
                 applicationId 'com.nextcloud.client'
+                dimension "default"
             }
 
             modified {
@@ -94,6 +100,7 @@ android {
                 // domain name
                 // .client
                 applicationId 'com.custom.client'
+                dimension "default"
             }
         }
 
@@ -111,11 +118,6 @@ android {
         preDexLibraries = preDexEnabled && !travisBuild
     }
 
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_7
-        targetCompatibility JavaVersion.VERSION_1_7
-    }
-
     packagingOptions {
         exclude 'META-INF/LICENSE.txt'
         exclude 'META-INF/LICENSE'
@@ -143,10 +145,10 @@ android {
             xml.enabled = false
             html.enabled = true
             xml {
-                destination "$project.buildDir/reports/pmd/pmd.xml"
+                destination = file("$project.buildDir/reports/pmd/pmd.xml")
             }
             html {
-                destination "$project.buildDir/reports/pmd/pmd.html"
+                destination = file("$project.buildDir/reports/pmd/pmd.html")
             }
         }
     }
@@ -165,67 +167,63 @@ android {
             xml.enabled = false
             html.enabled = true
             html {
-                destination "$project.buildDir/reports/findbugs/findbugs.html"
+                destination  = file("$project.buildDir/reports/findbugs/findbugs.html")
             }
         }
         classpath = files()
     }
     check.dependsOn 'checkstyle', 'findbugs', 'pmd', 'lint'
 
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
 }
 
 dependencies {
     /// dependencies for app building
-    compile name: 'touch-image-view'
-    compile 'com.android.support:multidex:1.0.1'
-
-    compile 'com.github.nextcloud:android-library:1.0.22'
-    compile "com.android.support:support-v4:${supportLibraryVersion}"
-    compile "com.android.support:design:${supportLibraryVersion}"
-    compile 'com.jakewharton:disklrucache:2.0.2'
-    compile "com.android.support:appcompat-v7:${supportLibraryVersion}"
-    compile "com.android.support:cardview-v7:${supportLibraryVersion}"
-    compile 'com.github.tobiasKaminsky:android-floating-action-button:1.10.2'
-    compile 'com.google.code.findbugs:annotations:2.0.1'
-    compile group: 'commons-io', name: 'commons-io', version: '2.4'
-    compile 'com.github.evernote:android-job:v1.1.9'
-    compile 'com.jakewharton:butterknife:8.4.0'
-    annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
-    compile 'org.greenrobot:eventbus:3.0.0'
-    compile 'com.googlecode.ez-vcard:ez-vcard:0.10.2'
-
+    implementation name: 'touch-image-view'
+    implementation 'com.android.support:multidex:1.0.2'
+    implementation 'com.github.nextcloud:android-library:1.0.23'
+    implementation "com.android.support:support-v4:${supportLibraryVersion}"
+    implementation "com.android.support:design:${supportLibraryVersion}"
+    implementation 'com.jakewharton:disklrucache:2.0.2'
+    implementation "com.android.support:appcompat-v7:${supportLibraryVersion}"
+    implementation "com.android.support:cardview-v7:${supportLibraryVersion}"
+    implementation "com.android.support:exifinterface:${supportLibraryVersion}"
+    implementation 'com.github.tobiasKaminsky:android-floating-action-button:1.10.2'
+    implementation 'com.google.code.findbugs:annotations:2.0.1'
+    implementation 'commons-io:commons-io:2.5'
+    implementation 'com.github.evernote:android-job:v1.1.11'
+    implementation 'com.jakewharton:butterknife:8.5.1'
+    annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'
+    implementation 'org.greenrobot:eventbus:3.0.0'
+    implementation 'com.googlecode.ez-vcard:ez-vcard:0.10.2'
+    implementation 'org.lukhnos:nnio:0.2'
     // uncomment for gplay, modified
-    // compile "com.google.firebase:firebase-messaging:${googleLibraryVersion}"
-    // compile "com.google.android.gms:play-services-base:${googleLibraryVersion}"
-
-    compile 'org.parceler:parceler-api:1.1.6'
+    // implementation "com.google.firebase:firebase-messaging:${googleLibraryVersion}"
+    // implementation "com.google.android.gms:play-services-base:${googleLibraryVersion}"
+    implementation 'org.parceler:parceler-api:1.1.6'
     annotationProcessor 'org.parceler:parceler:1.1.6'
-
-    compile 'com.github.bumptech.glide:glide:3.7.0'
-    compile 'com.caverock:androidsvg:1.2.1'
-
+    implementation 'com.github.bumptech.glide:glide:3.7.0'
+    implementation 'com.caverock:androidsvg:1.2.1'
     /// dependencies for local unit tests
-    testCompile 'junit:junit:4.12'
-    testCompile 'org.mockito:mockito-core:1.10.19'
-
+    testImplementation 'junit:junit:4.12'
+    testImplementation 'org.mockito:mockito-core:1.10.19'
     /// dependencies for instrumented tests
     // JUnit4 Rules
-    androidTestCompile 'com.android.support.test:rules:0.5'
-
+    androidTestImplementation 'com.android.support.test:rules:0.5'
     // Android JUnit Runner
-    androidTestCompile 'com.android.support.test:runner:0.5'
-
+    androidTestImplementation 'com.android.support.test:runner:0.5'
     // Android Annotation Support
-    androidTestCompile "com.android.support:support-annotations:${supportLibraryVersion}"
-
+    androidTestImplementation "com.android.support:support-annotations:${supportLibraryVersion}"
     // Espresso core
-    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
-
+    androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2'
     // UIAutomator - for cross-app UI tests, and to grant screen is turned on in Espresso tests
-    //androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
+    //androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
     // fix conflict in dependencies; see http://g.co/androidstudio/app-test-app-conflict for details
-    //androidTestCompile "com.android.support:support-annotations:${supportLibraryVersion}"
-
+    //androidTestImplementation "com.android.support:support-annotations:${supportLibraryVersion}"
+    implementation 'org.jetbrains:annotations:15.0'
 }
 
 configurations.all {

+ 1 - 0
drawable_resources/ic_folder_star.svg

@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M20,6H12L10,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8A2,2 0 0,0 20,6M17.94,17L15,15.28L12.06,17L12.84,13.67L10.25,11.43L13.66,11.14L15,8L16.34,11.14L19.75,11.43L17.16,13.67L17.94,17Z" /></svg>

+ 1 - 0
gradle.properties

@@ -0,0 +1 @@
+android.enableAapt2=false

+ 2 - 2
gradle/wrapper/gradle-wrapper.properties

@@ -1,6 +1,6 @@
-#Wed Mar 15 00:10:20 CET 2017
+#Fri May 05 19:09:31 CEST 2017
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip

+ 1 - 0
lint.xml

@@ -2,5 +2,6 @@
 <lint>
     <issue id="InvalidPackage">
         <ignore path="**/freemarker-2.3.23.jar"/>
+        <ignore path="**/nnio-0.2.jar"/>
     </issue>
 </lint>

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

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 10 errors and 676 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 3 errors and 653 warnings</span>

+ 2 - 2
src/gplay/java/com/owncloud/android/utils/PushUtils.java

@@ -167,7 +167,7 @@ public class PushUtils {
                     remoteOperationResult = unregisterAccountDeviceForProxyOperation.execute(mClient);
 
                     if (remoteOperationResult.isSuccess()) {
-                        arbitraryDataProvider.deleteKeyForAccount(account, KEY_PUSH);
+                        arbitraryDataProvider.deleteKeyForAccount(account.name, KEY_PUSH);
                     }
                 }
             }
@@ -244,7 +244,7 @@ public class PushUtils {
                                     PushConfigurationState pushArbitraryData = new PushConfigurationState(token,
                                             pushResponse.getDeviceIdentifier(), pushResponse.getSignature(),
                                             pushResponse.getPublicKey(), false);
-                                    arbitraryDataProvider.storeOrUpdateKeyValue(account, KEY_PUSH,
+                                    arbitraryDataProvider.storeOrUpdateKeyValue(account.name, KEY_PUSH,
                                             gson.toJson(pushArbitraryData));
                                 }
                             }

+ 8 - 34
src/main/AndroidManifest.xml

@@ -86,7 +86,7 @@
         <activity android:name=".ui.activity.ParticipateActivity" />
         <activity android:name=".ui.activity.ActivitiesListActivity"
                   android:configChanges="orientation|screenSize|keyboardHidden" />
-        <activity android:name=".ui.activity.FolderSyncActivity" />
+        <activity android:name=".ui.activity.SyncedFoldersActivity"/>
         <activity android:name=".ui.activity.UploadFilesActivity" />
         <activity android:name=".ui.activity.ExternalSiteWebView"
                   android:configChanges="orientation|screenSize|keyboardHidden" />
@@ -120,6 +120,11 @@
             android:label="@string/app_name"
             android:theme="@style/Theme.ownCloud.Fullscreen" />
 
+        <service
+            android:name=".jobs.NContentObserverJob"
+            android:permission="android.permission.BIND_JOB_SERVICE" >
+        </service>
+
         <service
             android:name=".authentication.AccountAuthenticatorService"
             android:exported="true" >
@@ -131,7 +136,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" >
@@ -160,7 +165,7 @@
             android:label="@string/search_users_and_groups_hint" />
 
         <provider
-            android:name="org.nextcloud.providers.DocumentsStorageProvider"
+            android:name=".providers.DocumentsStorageProvider"
             android:authorities="@string/document_provider_authority"
             android:exported="true"
             android:grantUriPermissions="true"
@@ -226,43 +231,12 @@
         <activity android:name=".ui.activity.UploadListActivity" />
         <activity android:name=".ui.activity.WhatsNewActivity"
                   android:theme="@style/Theme.ownCloud.noActionBar.Login" />
-        
-        <receiver android:name=".files.services.ConnectivityActionReceiver"
-		    android:enabled="true" android:label="ConnectivityActionReceiver">
-		    <intent-filter>
-		        <!--action android:name="android.net.conn.CONNECTIVITY_CHANGE"/-->
-		        <action android:name="android.net.wifi.STATE_CHANGE"/>
-                <action android:name="android.intent.action.ACTION_POWER_CONNECTED"/>
-                <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/>
-		    </intent-filter>
-		</receiver>
-		<receiver android:name=".files.InstantUploadBroadcastReceiver">
-            <intent-filter>
 
-                <!-- unofficially supported by many Android phones but not by HTC devices: -->
-                <action android:name="com.android.camera.NEW_PICTURE" />
-                <!-- officially supported since Android 4.0 (SDK 14, works even for HTC devices): -->
-                <action android:name="android.hardware.action.NEW_PICTURE" />
-
-                <data android:mimeType="image/*" />
-            </intent-filter>
-            <intent-filter>
-                <action android:name="android.hardware.action.NEW_VIDEO" />
-
-                <data android:mimeType="video/*" />
-            </intent-filter>
-        </receiver>
         <receiver android:name=".files.BootupBroadcastReceiver" >
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
             </intent-filter>
         </receiver>
-        <receiver android:name=".services.ShutdownReceiver">
-            <intent-filter>
-                <action android:name="android.intent.action.ACTION_SHUTDOWN" />
-                <action android:name="android.intent.action.QUICKBOOT_POWEROFF" />
-            </intent-filter>
-        </receiver>
 
 
         <service android:name=".services.observer.FileObserverService" />

+ 0 - 1
src/main/java/com/afollestad/sectionedrecyclerview/SectionedRecyclerViewAdapter.java

@@ -35,7 +35,6 @@ public abstract class SectionedRecyclerViewAdapter<VH extends RecyclerView.ViewH
 
     private final ArrayMap<Integer, Integer> mHeaderLocationMap;
     private GridLayoutManager mLayoutManager;
-    private ArrayMap<Integer, Integer> mSpanMap;
     private boolean mShowHeadersForEmptySections;
 
     public SectionedRecyclerViewAdapter() {

+ 143 - 38
src/main/java/com/owncloud/android/MainApp.java

@@ -19,34 +19,42 @@
  */
 package com.owncloud.android;
 
+import android.Manifest;
 import android.app.Activity;
-import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.content.DialogInterface;
 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.Build;
 import android.os.Bundle;
 import android.os.Environment;
-import android.os.IBinder;
 import android.support.multidex.MultiDexApplication;
 import android.support.v4.util.Pair;
+import android.support.v7.app.AlertDialog;
 
 import com.evernote.android.job.JobManager;
 import com.owncloud.android.authentication.PassCodeManager;
+import com.owncloud.android.datamodel.MediaFolder;
+import com.owncloud.android.datamodel.MediaFolderType;
+import com.owncloud.android.datamodel.MediaProvider;
 import com.owncloud.android.datamodel.SyncedFolder;
 import com.owncloud.android.datamodel.SyncedFolderProvider;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.db.PreferenceManager;
+import com.owncloud.android.jobs.NCJobCreator;
 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.NCJobCreator;
-import com.owncloud.android.services.observer.SyncedFolderObserverService;
 import com.owncloud.android.ui.activity.Preferences;
+import com.owncloud.android.ui.activity.SyncedFoldersActivity;
 import com.owncloud.android.ui.activity.WhatsNewActivity;
 import com.owncloud.android.utils.AnalyticsUtils;
+import com.owncloud.android.utils.FilesSyncHelper;
+import com.owncloud.android.utils.PermissionUtil;
+import com.owncloud.android.utils.ReceiversHelper;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -79,8 +87,6 @@ public class MainApp extends MultiDexApplication {
 
     private static boolean mOnlyOnDevice = false;
 
-    private static SyncedFolderObserverService mObserverService;
-
     @SuppressWarnings("unused")
     private boolean mBound;
 
@@ -118,14 +124,26 @@ public class MainApp extends MultiDexApplication {
             Log_OC.d("Debug", "start logging");
         }
 
+        updateToAutoUpload();
         cleanOldEntries();
         updateAutoUploadEntries();
 
-        Log_OC.d("SyncedFolderObserverService", "Start service SyncedFolderObserverService");
-        Intent i = new Intent(this, SyncedFolderObserverService.class);
-        startService(i);
-        bindService(i, syncedFolderObserverServiceConnection, Context.BIND_AUTO_CREATE);
+        if (PermissionUtil.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+            splitOutAutoUploadEntries();
+        } else {
+            PreferenceManager.setAutoUploadSplitEntries(this, true);
+        }
+
+        initiateExistingAutoUploadEntries();
+
+        FilesSyncHelper.scheduleFilesSyncIfNeeded();
+        FilesSyncHelper.restartJobsIfNeeded();
 
+        ReceiversHelper.registerNetworkChangeReceiver();
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            ReceiversHelper.registerPowerChangeReceiver();
+        }
 
         // register global protection with pass code
         registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@@ -245,10 +263,6 @@ public class MainApp extends MultiDexApplication {
         return mOnlyOnDevice;
     }
 
-    public static SyncedFolderObserverService getSyncedFolderObserverService() {
-        return mObserverService;
-    }
-
     // user agent
     public static String getUserAgent() {
         String appString = getAppContext().getResources().getString(R.string.user_agent);
@@ -271,6 +285,48 @@ public class MainApp extends MultiDexApplication {
         return userAgent;
     }
 
+    private void updateToAutoUpload() {
+            if (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_synced_folders)
+                        .setMessage(R.string.synced_folders_new_info)
+                        .setPositiveButton(R.string.drawer_open, new DialogInterface.OnClickListener() {
+                            public void onClick(DialogInterface dialog, int which) {
+                                // show Auto Upload
+                                Intent folderSyncIntent = new Intent(getApplicationContext(),
+                                        SyncedFoldersActivity.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.nav_synced_folders)
+                        .show();
+            }
+    }
+
     private void updateAutoUploadEntries() {
         // updates entries to reflect their true paths
         if (!PreferenceManager.getAutoUploadPathsUpdate(this)) {
@@ -280,6 +336,77 @@ public class MainApp extends MultiDexApplication {
         }
     }
 
+    private void splitOutAutoUploadEntries() {
+        if (!PreferenceManager.getAutoUploadSplitEntries(this)) {
+            // magic to split out existing synced folders in two when needed
+            // otherwise, we migrate them to their proper type (image or video)
+            Log_OC.i(TAG, "Migrate synced_folders records for image/video split");
+            ContentResolver contentResolver = this.getContentResolver();
+
+            SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(contentResolver);
+
+            final List<MediaFolder> imageMediaFolders = MediaProvider.getImageFolders(contentResolver, 1, null);
+            final List<MediaFolder> videoMediaFolders = MediaProvider.getVideoFolders(contentResolver, 1);
+
+            ArrayList<Long> idsToDelete = new ArrayList<>();
+            List<SyncedFolder> syncedFolders = syncedFolderProvider.getSyncedFolders();
+            long primaryKey;
+            SyncedFolder newSyncedFolder;
+            for (SyncedFolder syncedFolder : syncedFolders) {
+                idsToDelete.add(syncedFolder.getId());
+                Log_OC.i(TAG, "Migration check for synced_folders record: "
+                        + syncedFolder.getId() + " - " + syncedFolder.getLocalPath());
+
+                for (int i = 0; i < imageMediaFolders.size(); i++) {
+                    if (imageMediaFolders.get(i).absolutePath.equals(syncedFolder.getLocalPath())) {
+                        newSyncedFolder = (SyncedFolder) syncedFolder.clone();
+                        newSyncedFolder.setType(MediaFolderType.IMAGE);
+                        primaryKey = syncedFolderProvider.storeSyncedFolder(newSyncedFolder);
+                        Log_OC.i(TAG, "Migrated image synced_folders record: "
+                                + primaryKey + " - " + newSyncedFolder.getLocalPath());
+                        break;
+                    }
+                }
+
+                for (int j = 0; j < videoMediaFolders.size(); j++) {
+                    if (videoMediaFolders.get(j).absolutePath.equals(syncedFolder.getLocalPath())) {
+                        newSyncedFolder = (SyncedFolder) syncedFolder.clone();
+                        newSyncedFolder.setType(MediaFolderType.VIDEO);
+                        primaryKey = syncedFolderProvider.storeSyncedFolder(newSyncedFolder);
+                        Log_OC.i(TAG, "Migrated video synced_folders record: "
+                                + primaryKey + " - " + newSyncedFolder.getLocalPath());
+                        break;
+                    }
+                }
+            }
+
+            for (long id : idsToDelete) {
+                Log_OC.i(TAG, "Removing legacy synced_folders record: " + id);
+                syncedFolderProvider.deleteSyncedFolder(id);
+            }
+
+            PreferenceManager.setAutoUploadSplitEntries(this, true);
+        }
+    }
+
+    private void initiateExistingAutoUploadEntries() {
+        new Thread(() -> {
+            if (!PreferenceManager.getAutoUploadInit(getAppContext())) {
+                SyncedFolderProvider syncedFolderProvider =
+                        new SyncedFolderProvider(MainApp.getAppContext().getContentResolver());
+
+                for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
+                    if (syncedFolder.isEnabled()) {
+                        FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder);
+                    }
+                }
+
+                PreferenceManager.setAutoUploadInit(getAppContext(), true);
+            }
+
+        }).start();
+    }
+
     private void cleanOldEntries() {
         // previous versions of application created broken entries in the SyncedFolderProvider
         // database, and this cleans all that and leaves 1 (newest) entry per synced folder
@@ -302,9 +429,7 @@ public class MainApp extends MultiDexApplication {
                 }
             }
 
-            for (Long idValue : syncedFolders.values()) {
-                ids.add(idValue);
-            }
+            ids.addAll(syncedFolders.values());
 
             if (ids.size() > 0) {
                 syncedFolderProvider.deleteSyncedFoldersNotInList(mContext, ids);
@@ -313,24 +438,4 @@ public class MainApp extends MultiDexApplication {
             }
         }
     }
-
-    /**
-     * 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;
-        }
-    };
-
 }

+ 3 - 1
src/main/java/com/owncloud/android/authentication/PassCodeManager.java

@@ -31,6 +31,7 @@ import android.view.WindowManager;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.ui.activity.FingerprintActivity;
 import com.owncloud.android.ui.activity.PassCodeActivity;
+import com.owncloud.android.ui.activity.Preferences;
 
 import java.util.HashSet;
 import java.util.Set;
@@ -131,6 +132,7 @@ public class PassCodeManager {
 
     private boolean fingerprintIsEnabled() {
         SharedPreferences appPrefs = PreferenceManager.getDefaultSharedPreferences(MainApp.getAppContext());
-        return (appPrefs.getBoolean(FingerprintActivity.PREFERENCE_USE_FINGERPRINT, false));
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
+                appPrefs.getBoolean(Preferences.PREFERENCE_USE_FINGERPRINT, false);
     }
 }

+ 20 - 45
src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.java

@@ -47,36 +47,43 @@ public class ArbitraryDataProvider {
         this.contentResolver = contentResolver;
     }
 
-    public int deleteKeyForAccount(Account account, String key) {
-        int result = contentResolver.delete(
+    public int deleteKeyForAccount(String account, String key) {
+        return contentResolver.delete(
                 ProviderMeta.ProviderTableMeta.CONTENT_URI_ARBITRARY_DATA,
                 ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID + " = ? AND " +
                         ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_KEY + "= ?",
-                new String[]{account.name, key}
+                new String[]{account, key}
         );
+    }
 
-        return result;
+    public int deleteForKeyWhereAccountNotIn(ArrayList<String> accounts, String key) {
+        return contentResolver.delete(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_ARBITRARY_DATA,
+                ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID + " NOT IN (?) AND " +
+                        ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_KEY + "= ?",
+                new String[]{String.valueOf(accounts), key}
+        );
     }
 
 
-    public void storeOrUpdateKeyValue(Account account, String key, String newValue) {
-        ArbitraryDataSet data = getArbitraryDataSet(account, key);
+    public void storeOrUpdateKeyValue(String accountName, String key, String newValue) {
+        ArbitraryDataSet data = getArbitraryDataSet(accountName, key);
         if (data == null) {
-            Log_OC.v(TAG, "Adding arbitrary data with cloud id: " + account.name + " key: " + key
+            Log_OC.v(TAG, "Adding arbitrary data with cloud id: " + accountName + " key: " + key
                     + " value: " + newValue);
             ContentValues cv = new ContentValues();
-            cv.put(ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID, account.name);
+            cv.put(ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID, accountName);
             cv.put(ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_KEY, key);
             cv.put(ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_VALUE, newValue);
 
             Uri result = contentResolver.insert(ProviderMeta.ProviderTableMeta.CONTENT_URI_ARBITRARY_DATA, cv);
 
             if (result == null) {
-                Log_OC.v(TAG, "Failed to store arbitrary data with cloud id: " + account.name + " key: " + key
+                Log_OC.v(TAG, "Failed to store arbitrary data with cloud id: " + accountName + " key: " + key
                         + " value: " + newValue);
             }
         } else {
-            Log_OC.v(TAG, "Updating arbitrary data with cloud id: " + account.name + " key: " + key
+            Log_OC.v(TAG, "Updating arbitrary data with cloud id: " + accountName + " key: " + key
                     + " value: " + newValue);
             ContentValues cv = new ContentValues();
             cv.put(ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID, data.getCloudId());
@@ -91,7 +98,7 @@ public class ArbitraryDataProvider {
             );
 
             if (result == 0) {
-                Log_OC.v(TAG, "Failed to update arbitrary data with cloud id: " + account.name + " key: " + key
+                Log_OC.v(TAG, "Failed to update arbitrary data with cloud id: " + accountName + " key: " + key
                         + " value: " + newValue);
             }
         }
@@ -146,38 +153,6 @@ public class ArbitraryDataProvider {
         return getIntegerValue(account.name, key);
     }
 
-    private ArrayList<String> getValues(Account account, String key) {
-        Cursor cursor = contentResolver.query(
-                ProviderMeta.ProviderTableMeta.CONTENT_URI_ARBITRARY_DATA,
-                null,
-                ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID + " = ? and " +
-                        ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_KEY + " = ?",
-                new String[]{account.name, key},
-                null
-        );
-
-        if (cursor != null) {
-            ArrayList<String> list = new ArrayList<>();
-            if (cursor.moveToFirst()) {
-                do {
-                    String value = cursor.getString(cursor.getColumnIndex(
-                            ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_VALUE));
-                    if (value == null) {
-                        Log_OC.e(TAG, "Arbitrary value could not be created from cursor");
-                    } else {
-                        list.add(value);
-                    }
-                } while (cursor.moveToNext());
-            }
-            cursor.close();
-            return list;
-        } else {
-            Log_OC.e(TAG, "DB error restoring arbitrary values.");
-        }
-
-        return new ArrayList<>();
-    }
-
     /**
      * Returns stored value as string or empty string
      * @return string if value found or empty string
@@ -215,13 +190,13 @@ public class ArbitraryDataProvider {
         return "";
     }
 
-    private ArbitraryDataSet getArbitraryDataSet(Account account, String key) {
+    private ArbitraryDataSet getArbitraryDataSet(String accountName, String key) {
         Cursor cursor = contentResolver.query(
                 ProviderMeta.ProviderTableMeta.CONTENT_URI_ARBITRARY_DATA,
                 null,
                 ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID + " = ? and " +
                         ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_KEY + " = ?",
-                new String[]{account.name, key},
+                new String[]{accountName, key},
                 null
         );
 

+ 0 - 2
src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -593,7 +593,6 @@ public class FileDataStorageManager {
                 if (localFile.isDirectory()) {
                     success &= removeLocalFolder(localFile);
                 } else {
-                    String path = localFile.getAbsolutePath();
                     success &= localFile.delete();
                 }
             }
@@ -602,7 +601,6 @@ public class FileDataStorageManager {
         return success;
     }
 
-
     /**
      * Updates database and file system for a file or folder that was moved to a different location.
      *

+ 105 - 0
src/main/java/com/owncloud/android/datamodel/FileSystemDataSet.java

@@ -0,0 +1,105 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2017 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;
+
+/*
+    Model for filesystem data from the database
+ */
+public class FileSystemDataSet {
+
+    private int id;
+    private String localPath;
+    private long modifiedAt;
+    private boolean isFolder;
+    private boolean isSentForUpload;
+    private long foundAt;
+    private long syncedFolderId;
+
+    public FileSystemDataSet() {
+    }
+
+    public FileSystemDataSet(int id, String localPath, long modifiedAt, boolean isFolder,
+                             boolean isSentForUpload, long foundAt, long syncedFolderId) {
+        this.id = id;
+        this.localPath = localPath;
+        this.modifiedAt = modifiedAt;
+        this.isFolder = isFolder;
+        this.isSentForUpload = isSentForUpload;
+        this.foundAt = foundAt;
+        this.syncedFolderId = syncedFolderId;
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public String getLocalPath() {
+        return localPath;
+    }
+
+    public void setLocalPath(String localPath) {
+        this.localPath = localPath;
+    }
+
+    public long getModifiedAt() {
+        return modifiedAt;
+    }
+
+    public void setModifiedAt(long modifiedAt) {
+        this.modifiedAt = modifiedAt;
+    }
+
+    public boolean isFolder() {
+        return isFolder;
+    }
+
+    public void setFolder(boolean folder) {
+        isFolder = folder;
+    }
+
+    public long getFoundAt() {
+        return foundAt;
+    }
+
+    public void setFoundAt(long foundAt) {
+        this.foundAt = foundAt;
+    }
+
+    public boolean isSentForUpload() {
+        return isSentForUpload;
+    }
+
+    public void setSentForUpload(boolean sentForUpload) {
+        isSentForUpload = sentForUpload;
+    }
+
+    public long getSyncedFolderId() {
+        return syncedFolderId;
+    }
+
+    public void setSyncedFolderId(long syncedFolderId) {
+        this.syncedFolderId = syncedFolderId;
+    }
+}

+ 186 - 0
src/main/java/com/owncloud/android/datamodel/FilesystemDataProvider.java

@@ -0,0 +1,186 @@
+/**
+ * Nextcloud Android client application
+ *
+ * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2017 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 com.owncloud.android.db.ProviderMeta;
+import com.owncloud.android.lib.common.utils.Log_OC;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Provider for stored filesystem data.
+ */
+public class FilesystemDataProvider {
+
+    static private final String TAG = FilesystemDataProvider.class.getSimpleName();
+
+    private ContentResolver contentResolver;
+
+    public FilesystemDataProvider(ContentResolver contentResolver) {
+        if (contentResolver == null) {
+            throw new IllegalArgumentException("Cannot create an instance with a NULL contentResolver");
+        }
+        this.contentResolver = contentResolver;
+    }
+
+    public void updateFilesystemFileAsSentForUpload(String path, String syncedFolderId) {
+        ContentValues cv = new ContentValues();
+        cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD, 1);
+
+        contentResolver.update(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_FILESYSTEM,
+                cv,
+                ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH + " = ? and " +
+                        ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + " = ?",
+                new String[]{path, syncedFolderId}
+        );
+    }
+
+    public Set<String> getFilesForUpload(String localPath, String syncedFolderId) {
+        Set<String> localPathsToUpload = new HashSet<>();
+
+        String likeParam = localPath + "%";
+
+        Cursor cursor = contentResolver.query(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_FILESYSTEM,
+                null,
+                ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH + " LIKE ? and " +
+                        ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + " = ? and " +
+                        ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD + " = ? and " +
+                        ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER + " = ?",
+                new String[]{likeParam, syncedFolderId, "0", "0"},
+                null);
+
+        if (cursor != null && cursor.moveToFirst()) {
+            do {
+                String value = cursor.getString(cursor.getColumnIndex(
+                        ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH));
+                if (value == null) {
+                    Log_OC.e(TAG, "Cannot get local path");
+                } else {
+                    localPathsToUpload.add(value);
+                }
+            } while (cursor.moveToNext());
+
+            cursor.close();
+        }
+
+        return localPathsToUpload;
+    }
+
+    public void storeOrUpdateFileValue(String localPath, long modifiedAt, boolean isFolder, SyncedFolder syncedFolder) {
+
+        FileSystemDataSet data = getFilesystemDataSet(localPath, syncedFolder);
+
+        int isFolderValue = 0;
+        if (isFolder) {
+            isFolderValue = 1;
+        }
+
+        ContentValues cv = new ContentValues();
+        cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_FOUND_RECENTLY, System.currentTimeMillis());
+        cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_MODIFIED, modifiedAt);
+
+        if (data == null) {
+
+            cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH, localPath);
+            cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER, isFolderValue);
+            cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD, false);
+            cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID, syncedFolder.getId());
+
+            Uri result = contentResolver.insert(ProviderMeta.ProviderTableMeta.CONTENT_URI_FILESYSTEM, cv);
+
+            if (result == null) {
+                Log_OC.v(TAG, "Failed to insert filesystem data with local path: " + localPath);
+            }
+        } else {
+
+            if (data.getModifiedAt() != modifiedAt) {
+                cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD, 0);
+            }
+
+
+            int result = contentResolver.update(
+                    ProviderMeta.ProviderTableMeta.CONTENT_URI_FILESYSTEM,
+                    cv,
+                    ProviderMeta.ProviderTableMeta._ID + "=?",
+                    new String[]{String.valueOf(data.getId())}
+            );
+
+            if (result == 0) {
+                Log_OC.v(TAG, "Failed to update filesystem data with local path: " + localPath);
+            }
+        }
+    }
+
+    private FileSystemDataSet getFilesystemDataSet(String localPathParam, SyncedFolder syncedFolder) {
+
+        Cursor cursor = contentResolver.query(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_FILESYSTEM,
+                null,
+                ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH + " = ? and " +
+                        ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + " = ?",
+                new String[]{localPathParam, Long.toString(syncedFolder.getId())},
+                null
+        );
+
+        FileSystemDataSet dataSet = null;
+        if (cursor != null) {
+            if (cursor.moveToFirst()) {
+                int id = cursor.getInt(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta._ID));
+                String localPath = cursor.getString(cursor.getColumnIndex(
+                        ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH));
+                long modifiedAt = cursor.getLong(cursor.getColumnIndex(
+                        ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_MODIFIED));
+                boolean isFolder = false;
+                if (cursor.getInt(cursor.getColumnIndex(
+                        ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER)) != 0) {
+                    isFolder = true;
+                }
+                long foundAt = cursor.getLong(cursor.getColumnIndex(ProviderMeta.
+                        ProviderTableMeta.FILESYSTEM_FILE_FOUND_RECENTLY));
+
+                boolean isSentForUpload = false;
+                if (cursor.getInt(cursor.getColumnIndex(
+                        ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD)) != 0) {
+                    isSentForUpload = true;
+                }
+
+                if (id == -1) {
+                    Log_OC.e(TAG, "Arbitrary value could not be created from cursor");
+                } else {
+                    dataSet = new FileSystemDataSet(id, localPath, modifiedAt, isFolder, isSentForUpload, foundAt,
+                            syncedFolder.getId());
+                }
+            }
+            cursor.close();
+        } else {
+            Log_OC.e(TAG, "DB error restoring arbitrary values.");
+        }
+
+        return dataSet;
+    }
+}

+ 6 - 3
src/main/java/com/owncloud/android/datamodel/MediaFolder.java

@@ -4,17 +4,17 @@
  * @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/>.
  */
@@ -38,4 +38,7 @@ public class MediaFolder {
 
     /** total number of files in the media folder. */
     public long numberOfFiles;
+
+    /** type of media folder. */
+    public MediaFolderType type;
 }

+ 53 - 0
src/main/java/com/owncloud/android/datamodel/MediaFolderType.java

@@ -0,0 +1,53 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2017 Andy Scherzinger
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * 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.util.SparseArray;
+
+/**
+ * Types of media folder.
+ */
+public enum MediaFolderType {
+    CUSTOM(0),
+    IMAGE(1),
+    VIDEO(2);
+
+    private Integer id;
+
+    private static SparseArray<MediaFolderType> reverseMap = new SparseArray<>(3);
+
+    static {
+        reverseMap.put(CUSTOM.getId(), CUSTOM);
+        reverseMap.put(IMAGE.getId(), IMAGE);
+        reverseMap.put(VIDEO.getId(), VIDEO);
+    }
+
+    MediaFolderType(Integer id) {
+        this.id = id;
+    }
+
+    public static MediaFolderType getById(Integer id) {
+        return reverseMap.get(id);
+    }
+
+    public Integer getId() {
+        return id;
+    }
+}

+ 126 - 30
src/main/java/com/owncloud/android/datamodel/MediaProvider.java

@@ -40,6 +40,8 @@ import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 /**
  * Media queries to gain access to media lists for the device.
  */
@@ -47,12 +49,15 @@ 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 Uri IMAGES_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";
+    private static final String IMAGES_FILE_SELECTION = MediaStore.Images.Media.BUCKET_ID + "=";
+    private static final String[] IMAGES_FOLDER_PROJECTION = {"Distinct " + MediaStore.Images.Media.BUCKET_ID,
+            MediaStore.Images.Media.BUCKET_DISPLAY_NAME};
+    private static final String IMAGES_FOLDER_SORT_ORDER = MediaStore.Images.Media.BUCKET_DISPLAY_NAME + " ASC";
+
+    private static final String[] VIDEOS_FOLDER_PROJECTION = {"Distinct " + MediaStore.Video.Media.BUCKET_ID,
+            MediaStore.Video.Media.BUCKET_DISPLAY_NAME};
 
     /**
      * Getting All Images Paths.
@@ -61,11 +66,105 @@ public class MediaProvider {
      * @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,
-                                                    final Activity activity) {
+    public static List<MediaFolder> getImageFolders(ContentResolver contentResolver, int itemLimit,
+                                                    @Nullable final Activity activity) {
         // check permissions
-        if (!PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
+        checkPermissions(activity);
+
+
+        // query media/image folders
+        Cursor cursorFolders;
+        if (activity != null && PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
                 Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+            cursorFolders = contentResolver.query(
+                    IMAGES_MEDIA_URI,
+                    IMAGES_FOLDER_PROJECTION,
+                    null,
+                    null,
+                    IMAGES_FOLDER_SORT_ORDER
+            );
+        } else {
+            cursorFolders = contentResolver.query(
+                    IMAGES_MEDIA_URI,
+                    IMAGES_FOLDER_PROJECTION,
+                    null,
+                    null,
+                    IMAGES_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.type = MediaFolderType.IMAGE;
+                mediaFolder.folderName = folderName;
+                mediaFolder.filePaths = new ArrayList<>();
+
+                // query images
+                cursorImages = contentResolver.query(
+                        IMAGES_MEDIA_URI,
+                        FILE_PROJECTION,
+                        IMAGES_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));
+
+                        if (filePath != null) {
+                            mediaFolder.filePaths.add(filePath);
+                            mediaFolder.absolutePath = filePath.substring(0, filePath.lastIndexOf("/"));
+                        }
+                    }
+                    cursorImages.close();
+
+                    // only do further work if folder is not within the Nextcloud app itself
+                    if (!mediaFolder.absolutePath.startsWith(dataPath)) {
+
+                        // count images
+                        Cursor count = contentResolver.query(
+                                IMAGES_MEDIA_URI,
+                                FILE_PROJECTION,
+                                IMAGES_FILE_SELECTION + folderId,
+                                null,
+                                null);
+
+                        if (count != null) {
+                            mediaFolder.numberOfFiles = count.getCount();
+                            count.close();
+                        }
+
+                        mediaFolders.add(mediaFolder);
+                    }
+                }
+            }
+            cursorFolders.close();
+        }
+
+        return mediaFolders;
+    }
+
+    private static void checkPermissions(@Nullable Activity activity) {
+        if (activity != null &&
+                !PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
+                        Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
             // Check if we should show an explanation
             if (PermissionUtil.shouldShowRequestPermissionRationale(activity,
                     Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
@@ -87,49 +186,46 @@ public class MediaProvider {
                 PermissionUtil.requestWriteExternalStoreagePermission(activity);
             }
         }
+    }
 
-        // query media/image folders
-        Cursor cursorFolders = null;
-        if (PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
-                Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
-            cursorFolders = contentResolver.query(MEDIA_URI, FOLDER_PROJECTION, null, null, FOLDER_SORT_ORDER);
-        }
+    public static List<MediaFolder> getVideoFolders(ContentResolver contentResolver, int itemLimit) {
+        Cursor cursorFolders = contentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+                VIDEOS_FOLDER_PROJECTION, null, null, null);
         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;
+            String fileSortOrder = MediaStore.Video.Media.DATE_TAKEN + " DESC LIMIT " + itemLimit;
             Cursor cursorImages;
 
             while (cursorFolders.moveToNext()) {
-                String folderId = cursorFolders.getString(cursorFolders.getColumnIndex(MediaStore.Images.Media
+                String folderId = cursorFolders.getString(cursorFolders.getColumnIndex(MediaStore.Video.Media
                         .BUCKET_ID));
 
                 MediaFolder mediaFolder = new MediaFolder();
                 folderName = cursorFolders.getString(cursorFolders.getColumnIndex(
-                        MediaStore.Images.Media.BUCKET_DISPLAY_NAME));
+                        MediaStore.Video.Media.BUCKET_DISPLAY_NAME));
+                mediaFolder.type = MediaFolderType.VIDEO;
                 mediaFolder.folderName = folderName;
                 mediaFolder.filePaths = new ArrayList<>();
 
                 // query images
-                cursorImages = contentResolver.query(MEDIA_URI, FILE_PROJECTION, FILE_SELECTION + folderId, null,
+                cursorImages = contentResolver.query(
+                        MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+                        FILE_PROJECTION,
+                        MediaStore.Video.Media.BUCKET_ID + "=" + folderId,
+                        null,
                         fileSortOrder);
-                Log.d(TAG, "Reading images for " + mediaFolder.folderName);
+                Log.d(TAG, "Reading videos for " + mediaFolder.folderName);
 
                 if (cursorImages != null) {
                     String filePath;
-                    int failedImages = 0;
                     while (cursorImages.moveToNext()) {
                         filePath = cursorImages.getString(cursorImages.getColumnIndexOrThrow(
                                 MediaStore.MediaColumns.DATA));
-
-                        if (filePath != null) {
-                            mediaFolder.filePaths.add(filePath);
-                            mediaFolder.absolutePath = filePath.substring(0, filePath.lastIndexOf("/"));
-                        } else {
-                            failedImages++;
-                        }
+                        mediaFolder.filePaths.add(filePath);
+                        mediaFolder.absolutePath = filePath.substring(0, filePath.lastIndexOf("/"));
                     }
                     cursorImages.close();
 
@@ -138,14 +234,14 @@ public class MediaProvider {
 
                         // count images
                         Cursor count = contentResolver.query(
-                                MEDIA_URI,
+                                MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
                                 FILE_PROJECTION,
-                                FILE_SELECTION + folderId,
+                                MediaStore.Video.Media.BUCKET_ID + "=" + folderId,
                                 null,
                                 null);
 
                         if (count != null) {
-                            mediaFolder.numberOfFiles = count.getCount() - failedImages;
+                            mediaFolder.numberOfFiles = count.getCount();
                             count.close();
                         }
 

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

@@ -26,7 +26,7 @@ import java.io.Serializable;
 /**
  * Synced folder entity containing all information per synced folder.
  */
-public class SyncedFolder implements Serializable {
+public class SyncedFolder implements Serializable, Cloneable {
     public static final long UNPERSISTED_ID = Long.MIN_VALUE;
     private static final long serialVersionUID = -793476118299906429L;
     private long id = UNPERSISTED_ID;
@@ -38,6 +38,7 @@ public class SyncedFolder implements Serializable {
     private String account;
     private Integer uploadAction;
     private boolean enabled;
+    private MediaFolderType type;
 
     /**
      * constructor for already persisted entity.
@@ -51,9 +52,11 @@ public class SyncedFolder implements Serializable {
      * @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 type            the type of the folder
      */
     public SyncedFolder(long id, String localPath, String remotePath, Boolean wifiOnly, Boolean chargingOnly,
-                        Boolean subfolderByDate, String account, Integer uploadAction, Boolean enabled) {
+                        Boolean subfolderByDate, String account, Integer uploadAction, Boolean enabled,
+                        MediaFolderType type) {
         this.id = id;
         this.localPath = localPath;
         this.remotePath = remotePath;
@@ -63,6 +66,7 @@ public class SyncedFolder implements Serializable {
         this.account = account;
         this.uploadAction = uploadAction;
         this.enabled = enabled;
+        this.type = type;
     }
 
     /**
@@ -76,9 +80,11 @@ public class SyncedFolder implements Serializable {
      * @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 type            the type of the folder
      */
     public SyncedFolder(String localPath, String remotePath, Boolean wifiOnly, Boolean chargingOnly,
-                        Boolean subfolderByDate, String account, Integer uploadAction, Boolean enabled) {
+                        Boolean subfolderByDate, String account, Integer uploadAction, Boolean enabled,
+                        MediaFolderType type) {
         this.localPath = localPath;
         this.remotePath = remotePath;
         this.wifiOnly = wifiOnly;
@@ -87,6 +93,15 @@ public class SyncedFolder implements Serializable {
         this.account = account;
         this.uploadAction = uploadAction;
         this.enabled = enabled;
+        this.type = type;
+    }
+
+    public Object clone() {
+        try {
+            return super.clone();
+        } catch (CloneNotSupportedException e) {
+            return null;
+        }
     }
 
     public long getId() {
@@ -160,4 +175,12 @@ public class SyncedFolder implements Serializable {
     public void setEnabled(boolean enabled) {
         this.enabled = enabled;
     }
+
+    public MediaFolderType getType() {
+        return type;
+    }
+
+    public void setType(MediaFolderType type) {
+        this.type = type;
+    }
 }

+ 6 - 5
src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java

@@ -47,11 +47,13 @@ public class SyncedFolderDisplayItem extends SyncedFolder {
      * @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
+     * @param type            the type of 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);
+                                   List<String> filePaths, String folderName, long numberOfFiles, MediaFolderType type)
+    {
+        super(id, localPath, remotePath, wifiOnly, chargingOnly, subfolderByDate, account, uploadAction, enabled, type);
         this.filePaths = filePaths;
         this.folderName = folderName;
         this.numberOfFiles = numberOfFiles;
@@ -59,12 +61,11 @@ public class SyncedFolderDisplayItem extends SyncedFolder {
 
     public SyncedFolderDisplayItem(long id, String localPath, String remotePath, Boolean wifiOnly, Boolean chargingOnly,
                                    Boolean subfolderByDate, String account, Integer uploadAction, Boolean enabled,
-                                   String folderName) {
-        super(id, localPath, remotePath, wifiOnly, chargingOnly, subfolderByDate, account, uploadAction, enabled);
+                                   String folderName, MediaFolderType type) {
+        super(id, localPath, remotePath, wifiOnly, chargingOnly, subfolderByDate, account, uploadAction, enabled, type);
         this.folderName = folderName;
     }
 
-
     public List<String> getFilePaths() {
         return filePaths;
     }

+ 36 - 42
src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java

@@ -1,21 +1,22 @@
 /**
- *   Nextcloud Android client application
+ * Nextcloud Android client application
  *
- *   Copyright (C) 2016 Andy Scherzinger
- *   Copyright (C) 2016 Nextcloud.
+ * @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 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.
+ * 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/>.
+ * 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;
 
@@ -27,7 +28,6 @@ import android.database.Cursor;
 import android.net.Uri;
 import android.support.annotation.NonNull;
 
-import com.owncloud.android.MainApp;
 import com.owncloud.android.db.PreferenceManager;
 import com.owncloud.android.db.ProviderMeta;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -58,12 +58,12 @@ public class SyncedFolderProvider extends Observable {
     }
 
     /**
-     * Stores an media folder sync object in database.
+     * Stores a synced folder object in database.
      *
      * @param syncedFolder synced folder to store
      * @return synced folder id, -1 if the insert process fails.
      */
-    public long storeFolderSync(SyncedFolder syncedFolder) {
+    public long storeSyncedFolder(SyncedFolder syncedFolder) {
         Log_OC.v(TAG, "Inserting " + syncedFolder.getLocalPath() + " with enabled=" + syncedFolder.isEnabled());
 
         ContentValues cv = createContentValuesFromSyncedFolder(syncedFolder);
@@ -71,7 +71,6 @@ public class SyncedFolderProvider extends Observable {
         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.");
@@ -118,12 +117,12 @@ public class SyncedFolderProvider extends Observable {
     /**
      * Update upload status of file uniquely referenced by id.
      *
-     * @param id      folder sync id.
+     * @param id      synced folder 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);
+    public int updateSyncedFolderEnabled(long id, Boolean enabled) {
+        Log_OC.v(TAG, "Storing synced folder id" + id + " with enabled=" + enabled);
 
         int result = 0;
         Cursor cursor = mContentResolver.query(
@@ -187,7 +186,6 @@ public class SyncedFolderProvider extends Observable {
         }
 
         return result;
-
     }
 
     /**
@@ -211,15 +209,12 @@ public class SyncedFolderProvider extends Observable {
      *
      * @param id for the synced folder.
      */
-
     private int deleteSyncFolderWithId(long id) {
-        int result = mContentResolver.delete(
+        return mContentResolver.delete(
                 ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
                 ProviderMeta.ProviderTableMeta._ID + " = ?",
                 new String[]{String.valueOf(id)}
         );
-
-        return result;
     }
 
 
@@ -274,6 +269,17 @@ public class SyncedFolderProvider extends Observable {
         return result;
     }
 
+    /**
+     * delete record of synchronized folder with the given id.
+     */
+    public int deleteSyncedFolder(long id) {
+        return mContentResolver.delete(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
+                ProviderMeta.ProviderTableMeta._ID + " = ?",
+                new String[]{String.valueOf(id)}
+        );
+    }
+
     /**
      * update given synced folder.
      *
@@ -292,10 +298,6 @@ public class SyncedFolderProvider extends Observable {
                 new String[]{String.valueOf(syncedFolder.getId())}
         );
 
-        if (result > 0) {
-            notifyFolderSyncObservers(syncedFolder);
-        }
-
         return result;
     }
 
@@ -325,9 +327,11 @@ public class SyncedFolderProvider extends Observable {
                     ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_UPLOAD_ACTION));
             Boolean enabled = cursor.getInt(cursor.getColumnIndex(
                     ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED)) == 1;
+            MediaFolderType type = MediaFolderType.getById(cursor.getInt(cursor.getColumnIndex(
+                    ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_TYPE)));
 
             syncedFolder = new SyncedFolder(id, localPath, remotePath, wifiOnly, chargingOnly, subfolderByDate,
-                    accountName, uploadAction, enabled);
+                    accountName, uploadAction, enabled, type);
         }
         return syncedFolder;
     }
@@ -349,18 +353,8 @@ public class SyncedFolderProvider extends Observable {
         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;
-    }
+        cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_TYPE, syncedFolder.getType().getId());
 
-    /**
-     * Inform all observers about data change.
-     *
-     * @param syncedFolder changed, synchronized folder
-     */
-    private void notifyFolderSyncObservers(SyncedFolder syncedFolder) {
-        if (syncedFolder != null) {
-            MainApp.getSyncedFolderObserverService().restartObserver(syncedFolder);
-            Log_OC.d(TAG, "notifying folder sync data observers for changed/added: " + syncedFolder.getLocalPath());
-        }
+        return cv;
     }
 }

+ 41 - 6
src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java

@@ -30,6 +30,7 @@ import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
+import android.media.MediaMetadataRetriever;
 import android.media.ThumbnailUtils;
 import android.net.Uri;
 import android.os.AsyncTask;
@@ -418,6 +419,7 @@ public class ThumbnailsCacheManager {
     }
 
     public static class MediaThumbnailGenerationTask extends AsyncTask<Object, Void, Bitmap> {
+        private enum Type {IMAGE, VIDEO}
         private final WeakReference<ImageView> mImageViewReference;
         private File mFile;
         private String mImageKey = null;
@@ -439,7 +441,9 @@ public class ThumbnailsCacheManager {
                     }
 
                     if (MimeTypeUtil.isImage(mFile)) {
-                        thumbnail = doFileInBackground(mFile);
+                        thumbnail = doFileInBackground(mFile, Type.IMAGE);
+                    } else if (MimeTypeUtil.isVideo(mFile)) {
+                        thumbnail = doFileInBackground(mFile, Type.VIDEO);
                     }
                 }
             } // the app should never break due to a problem with thumbnails
@@ -482,7 +486,7 @@ public class ThumbnailsCacheManager {
             }
         }
 
-        private Bitmap doFileInBackground(File file) {
+        private Bitmap doFileInBackground(File file, Type type) {
             final String imageKey;
 
             if (mImageKey != null) {
@@ -497,14 +501,45 @@ public class ThumbnailsCacheManager {
             // Not found in disk cache
             if (thumbnail == null) {
 
-                int px = getThumbnailDimension();
+                if (Type.IMAGE.equals(type)) {
+                    int px = getThumbnailDimension();
 
-                Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(file.getAbsolutePath(), px, px);
+                    Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(file.getAbsolutePath(), px, px);
 
-                if (bitmap != null) {
-                    thumbnail = addThumbnailToCache(imageKey, bitmap, file.getPath(), px);
+                    if (bitmap != null) {
+                        thumbnail = addThumbnailToCache(imageKey, bitmap, file.getPath(), px);
+                    }
+                } else if (Type.VIDEO.equals(type)) {
+                    MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+                    try {
+                        retriever.setDataSource(file.getAbsolutePath());
+                        thumbnail = retriever.getFrameAtTime(-1);
+                    } catch (Exception ex) {
+                        // can't create a bitmap
+                        Log_OC.w(TAG, "Failed to create bitmap from video " + file.getAbsolutePath());
+                    } finally {
+                        try {
+                            retriever.release();
+                        } catch (RuntimeException ex) {
+                            // Ignore failure at this point.
+                            Log_OC.w(TAG, "Failed release MediaMetadataRetriever for " + file.getAbsolutePath());
+                        }
+                    }
+
+                    if (thumbnail != null) {
+                        // Scale down bitmap if too large.
+                        int px = getThumbnailDimension();
+                        int width = thumbnail.getWidth();
+                        int height = thumbnail.getHeight();
+                        int max = Math.max(width, height);
+                        if (max > px) {
+                            thumbnail = BitmapUtils.scaleBitmap(thumbnail, px, width, height, max);
+                            thumbnail = addThumbnailToCache(imageKey, thumbnail, file.getPath(), px);
+                        }
+                    }
                 }
             }
+
             return thumbnail;
         }
     }

+ 127 - 143
src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java

@@ -1,22 +1,22 @@
 /**
- *  ownCloud Android client application
+ * ownCloud Android client application
  *
- *  @author LukeOwncloud
- *  @author David A. Velasco
- *  @author masensio
- *  Copyright (C) 2016 ownCloud Inc.
+ * @author LukeOwncloud
+ * @author David A. Velasco
+ * @author masensio
+ * Copyright (C) 2016 ownCloud Inc.
  *
- *  This program is free software: you can redistribute it and/or modify
- *  it under the terms of the GNU General Public License version 2,
- *  as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
  *
- *  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 General Public License for more details.
+ * 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 General Public License for more details.
  *
- *  You should have received a copy of the GNU General Public License
- *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 package com.owncloud.android.datamodel;
 
@@ -26,12 +26,8 @@ import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.Build;
-import android.support.annotation.RequiresApi;
 
-import com.evernote.android.job.JobManager;
-import com.evernote.android.job.JobRequest;
-import com.evernote.android.job.util.support.PersistableBundleCompat;
+import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
 import com.owncloud.android.db.UploadResult;
@@ -39,14 +35,9 @@ import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.operations.UploadFileOperation;
-import com.owncloud.android.services.AutoUploadJob;
 
-import java.util.ArrayList;
 import java.util.Calendar;
-import java.util.Collections;
-import java.util.List;
 import java.util.Observable;
-import java.util.Set;
 
 /**
  * Database helper for storing list of files to be uploaded, including status
@@ -130,6 +121,8 @@ public class UploadsStorageManager extends Observable {
         cv.put(ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER, ocUpload.isCreateRemoteFolder() ? 1 : 0);
         cv.put(ProviderTableMeta.UPLOADS_LAST_RESULT, ocUpload.getLastResult().getValue());
         cv.put(ProviderTableMeta.UPLOADS_CREATED_BY, ocUpload.getCreadtedBy());
+        cv.put(ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY, ocUpload.isWhileChargingOnly() ? 1 : 0);
+        cv.put(ProviderTableMeta.UPLOADS_IS_WIFI_ONLY, ocUpload.isUseWifiOnly() ? 1 : 0);
 
         Uri result = getDB().insert(ProviderTableMeta.CONTENT_URI_UPLOADS, cv);
 
@@ -163,9 +156,9 @@ public class UploadsStorageManager extends Observable {
         cv.put(ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP, ocUpload.getUploadEndTimestamp());
 
         int result = getDB().update(ProviderTableMeta.CONTENT_URI_UPLOADS,
-            cv,
-            ProviderTableMeta._ID + "=?",
-            new String[]{String.valueOf(ocUpload.getUploadId())}
+                cv,
+                ProviderTableMeta._ID + "=?",
+                new String[]{String.valueOf(ocUpload.getUploadId())}
         );
 
         Log_OC.d(TAG, "updateUpload returns with: " + result + " for file: " + ocUpload.getLocalPath());
@@ -188,15 +181,15 @@ public class UploadsStorageManager extends Observable {
 
             String path = c.getString(c.getColumnIndex(ProviderTableMeta.UPLOADS_LOCAL_PATH));
             Log_OC.v(
-                TAG,
-                "Updating " + path + " with status:" + status + " and result:"
-                    + (result == null ? "null" : result.toString()) + " (old:"
-                    + upload.toFormattedString() + ")");
+                    TAG,
+                    "Updating " + path + " with status:" + status + " and result:"
+                            + (result == null ? "null" : result.toString()) + " (old:"
+                            + upload.toFormattedString() + ")");
 
             upload.setUploadStatus(status);
             upload.setLastResult(result);
             upload.setRemotePath(remotePath);
-            if(localPath != null) {
+            if (localPath != null) {
                 upload.setLocalPath(localPath);
             }
             if (status == UploadStatus.UPLOAD_SUCCEEDED) {
@@ -221,7 +214,7 @@ public class UploadsStorageManager extends Observable {
      * @param localPath path of the file to upload in the device storage
      * @return 1 if file status was updated, else 0.
      */
-    public int updateUploadStatus(long id, UploadStatus status, UploadResult result, String remotePath,
+    private int updateUploadStatus(long id, UploadStatus status, UploadResult result, String remotePath,
                                   String localPath) {
         //Log_OC.v(TAG, "Updating "+filepath+" with uploadStatus="+status +" and result="+result);
 
@@ -236,7 +229,7 @@ public class UploadsStorageManager extends Observable {
 
         if (c.getCount() != 1) {
             Log_OC.e(TAG, c.getCount() + " items for id=" + id
-                + " available in UploadDb. Expected 1. Failed to update upload db.");
+                    + " available in UploadDb. Expected 1. Failed to update upload db.");
         } else {
             returnValue = updateUploadInternal(c, status, result, remotePath, localPath);
         }
@@ -266,7 +259,7 @@ public class UploadsStorageManager extends Observable {
     public int removeUpload(OCUpload upload) {
         int result = getDB().delete(
                 ProviderTableMeta.CONTENT_URI_UPLOADS,
-                ProviderTableMeta._ID + "=?" ,
+                ProviderTableMeta._ID + "=?",
                 new String[]{Long.toString(upload.getUploadId())}
         );
         Log_OC.d(TAG, "delete returns " + result + " for upload " + upload);
@@ -287,7 +280,7 @@ public class UploadsStorageManager extends Observable {
     public int removeUpload(String accountName, String remotePath) {
         int result = getDB().delete(
                 ProviderTableMeta.CONTENT_URI_UPLOADS,
-                ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "=? AND " + ProviderTableMeta.UPLOADS_REMOTE_PATH + "=?" ,
+                ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "=? AND " + ProviderTableMeta.UPLOADS_REMOTE_PATH + "=?",
                 new String[]{accountName, remotePath}
         );
         Log_OC.d(TAG, "delete returns " + result + " for file " + remotePath + " in " + accountName);
@@ -374,68 +367,21 @@ public class UploadsStorageManager extends Observable {
         return upload;
     }
 
-    /**
-     * Get all uploads which are currently being uploaded or waiting in the queue to be uploaded.
-     */
-    public OCUpload[] getCurrentAndPendingUploads() {
+    public OCUpload[] getCurrentAndPendingUploadsForCurrentAccount() {
+        Account account = AccountUtils.getCurrentOwnCloudAccount(mContext);
 
         OCUpload[] uploads = getUploads(
-            ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_IN_PROGRESS.value + " OR " +
-            ProviderTableMeta.UPLOADS_LAST_RESULT + "==" + UploadResult.DELAYED_FOR_WIFI.getValue() + " OR " +
-            ProviderTableMeta.UPLOADS_LAST_RESULT + "==" + UploadResult.DELAYED_FOR_CHARGING.getValue(),
-            null
+                ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_IN_PROGRESS.value +
+                        " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT + "==" + UploadResult.DELAYED_FOR_WIFI.getValue() +
+                        " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT + "==" + UploadResult.LOCK_FAILED.getValue() +
+                        " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT +
+                        "==" + UploadResult.DELAYED_FOR_CHARGING.getValue() +
+                        " AND " + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?",
+                new String[]{account.name}
         );
 
-        // add pending Jobs
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
-            return uploads;
-        } else {
-            List<OCUpload> result = getPendingJobs();
-            Collections.addAll(result, uploads);
-            return result.toArray(uploads);
-        }
-    }
-
-    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-    private List<OCUpload> getPendingJobs() {
-        Set<JobRequest> jobRequests = JobManager.create(mContext).getAllJobRequestsForTag(AutoUploadJob.TAG);
-
-        ArrayList<OCUpload> list = new ArrayList<>();
-
-        for (JobRequest ji : jobRequests) {
-            PersistableBundleCompat extras = ji.getExtras();
-                OCUpload upload = new OCUpload(extras.getString("filePath", ""),
-                        extras.getString("remotePath", ""),
-                        extras.getString("account", ""));
-
-                list.add(upload);
-        }
-
-        return list;
-    }
+        return uploads;
 
-    public void cancelPendingAutoUploadJobsForAccount(Account account) {
-        JobManager jobManager = JobManager.create(mContext);
-        for (JobRequest ji: jobManager.getAllJobRequestsForTag(AutoUploadJob.TAG)) {
-            if (ji.getExtras().getString(AutoUploadJob.ACCOUNT, "").equalsIgnoreCase(account.name)) {
-                jobManager.cancel(ji.getJobId());
-            }
-        }
-    }
-
-    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-    public void cancelPendingJob(String accountName, String remotePath){
-        JobManager jobManager = JobManager.create(mContext);
-        Set<JobRequest> jobRequests = jobManager.getAllJobRequests();
-
-        for (JobRequest ji : jobRequests) {
-            PersistableBundleCompat extras = ji.getExtras();
-            if (remotePath.equalsIgnoreCase(extras.getString("remotePath", "")) &&
-                    accountName.equalsIgnoreCase(extras.getString("account", ""))) {
-                jobManager.cancel(ji.getJobId());
-                break;
-            }
-        }
     }
 
     /**
@@ -447,6 +393,13 @@ public class UploadsStorageManager extends Observable {
                 ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value, null);
     }
 
+    public OCUpload[] getFinishedUploadsForCurrentAccount() {
+        Account account = AccountUtils.getCurrentOwnCloudAccount(mContext);
+
+        return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value + AND +
+                ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", new String[]{account.name});
+    }
+
     /**
      * Get all uploads which where successfully completed.
      */
@@ -455,16 +408,33 @@ public class UploadsStorageManager extends Observable {
         return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value, null);
     }
 
+    public OCUpload[] getFailedButNotDelayedUploadsForCurrentAccount() {
+        Account account = AccountUtils.getCurrentOwnCloudAccount(mContext);
+
+        return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value +
+                        AND +
+                        ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() +
+                        AND +
+                        ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.LOCK_FAILED.getValue() +
+                        AND +
+                        ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() +
+                        AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?",
+                new String[]{account.name}
+        );
+    }
+
     /**
-     * Get all failed uploads, except for those that were not performed due to lack of Wifi connection
-     * @return      Array of failed uploads, except for those that were not performed due to lack of Wifi connection.
+     * Get all failed uploads, except for those that were not performed due to lack of Wifi connection.
+     *
+     * @return Array of failed uploads, except for those that were not performed due to lack of Wifi connection.
      */
     public OCUpload[] getFailedButNotDelayedUploads() {
 
         return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value + AND +
-                ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() + AND +
-                ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue(),
-            null
+                        ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.LOCK_FAILED.getValue() + AND +
+                        ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() + AND +
+                        ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue(),
+                null
         );
     }
 
@@ -473,13 +443,22 @@ public class UploadsStorageManager extends Observable {
     }
 
     public long clearFailedButNotDelayedUploads() {
+        Account account = AccountUtils.getCurrentOwnCloudAccount(mContext);
+
         long result = getDB().delete(
-            ProviderTableMeta.CONTENT_URI_UPLOADS,
-                ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value + AND +
-                ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() + AND +
-                ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue(),
-            null
+                ProviderTableMeta.CONTENT_URI_UPLOADS,
+                ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value +
+                        AND +
+                        ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.LOCK_FAILED.getValue() +
+                        AND +
+                        ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() +
+                        AND +
+                        ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() +
+                        AND +
+                        ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?",
+                new String[]{account.name}
         );
+
         Log_OC.d(TAG, "delete all failed uploads but those delayed for Wifi");
         if (result > 0) {
             notifyObserversNow();
@@ -488,11 +467,14 @@ public class UploadsStorageManager extends Observable {
     }
 
     public long clearSuccessfulUploads() {
+        Account account = AccountUtils.getCurrentOwnCloudAccount(mContext);
 
         long result = getDB().delete(
                 ProviderTableMeta.CONTENT_URI_UPLOADS,
-                ProviderTableMeta.UPLOADS_STATUS + "=="+ UploadStatus.UPLOAD_SUCCEEDED.value, null
+                ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value  + AND +
+                ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", new String[]{account.name}
         );
+
         Log_OC.d(TAG, "delete all successful uploads");
         if (result > 0) {
             notifyObserversNow();
@@ -501,21 +483,31 @@ public class UploadsStorageManager extends Observable {
     }
 
     public long clearAllFinishedButNotDelayedUploads() {
+        Account account = AccountUtils.getCurrentOwnCloudAccount(mContext);
 
-        String[] whereArgs = new String[2];
+        String[] whereArgs = new String[3];
         whereArgs[0] = String.valueOf(UploadStatus.UPLOAD_SUCCEEDED.value);
         whereArgs[1] = String.valueOf(UploadStatus.UPLOAD_FAILED.value);
+        whereArgs[2] = account.name;
         long result = getDB().delete(
                 ProviderTableMeta.CONTENT_URI_UPLOADS,
-                ProviderTableMeta.UPLOADS_STATUS + "=? OR " + ProviderTableMeta.UPLOADS_STATUS + "=? AND " +
-                ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() + AND +
-                ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue(),
+                ProviderTableMeta.UPLOADS_STATUS + "=? OR " + ProviderTableMeta.UPLOADS_STATUS + "=?" +
+                        AND +
+                        ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.LOCK_FAILED.getValue() +
+                        AND +
+                        ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() +
+                        AND +
+                        ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() +
+                        AND +
+                        ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?",
                 whereArgs
         );
+
         Log_OC.d(TAG, "delete all finished uploads");
         if (result > 0) {
             notifyObserversNow();
         }
+
         return result;
     }
 
@@ -528,28 +520,28 @@ public class UploadsStorageManager extends Observable {
 
         if (uploadResult.isCancelled()) {
             removeUpload(
-                upload.getAccount().name,
-                upload.getRemotePath()
+                    upload.getAccount().name,
+                    upload.getRemotePath()
             );
         } else {
             String localPath = (FileUploader.LOCAL_BEHAVIOUR_MOVE == upload.getLocalBehaviour())
-                ? upload.getStoragePath() : null;
+                    ? upload.getStoragePath() : null;
 
             if (uploadResult.isSuccess()) {
                 updateUploadStatus(
-                    upload.getOCUploadId(),
-                    UploadStatus.UPLOAD_SUCCEEDED,
-                    UploadResult.UPLOADED,
-                    upload.getRemotePath(),
-                    localPath
+                        upload.getOCUploadId(),
+                        UploadStatus.UPLOAD_SUCCEEDED,
+                        UploadResult.UPLOADED,
+                        upload.getRemotePath(),
+                        localPath
                 );
             } else {
                 updateUploadStatus(
-                    upload.getOCUploadId(),
-                    UploadStatus.UPLOAD_FAILED,
-                    UploadResult.fromOperationResult(uploadResult),
-                    upload.getRemotePath(),
-                    localPath
+                        upload.getOCUploadId(),
+                        UploadStatus.UPLOAD_FAILED,
+                        UploadResult.fromOperationResult(uploadResult),
+                        upload.getRemotePath(),
+                        localPath
                 );
             }
         }
@@ -560,14 +552,14 @@ public class UploadsStorageManager extends Observable {
      */
     public void updateDatabaseUploadStart(UploadFileOperation upload) {
         String localPath = (FileUploader.LOCAL_BEHAVIOUR_MOVE == upload.getLocalBehaviour())
-            ? upload.getStoragePath() : null;
+                ? upload.getStoragePath() : null;
 
         updateUploadStatus(
-            upload.getOCUploadId(),
-            UploadStatus.UPLOAD_IN_PROGRESS,
-            UploadResult.UNKNOWN,
-            upload.getRemotePath(),
-            localPath
+                upload.getOCUploadId(),
+                UploadStatus.UPLOAD_IN_PROGRESS,
+                UploadResult.UNKNOWN,
+                upload.getRemotePath(),
+                localPath
         );
     }
 
@@ -576,7 +568,7 @@ public class UploadsStorageManager extends Observable {
      * Changes the status of any in progress upload from UploadStatus.UPLOAD_IN_PROGRESS
      * to UploadStatus.UPLOAD_FAILED
      *
-     * @return      Number of uploads which status was changed.
+     * @return Number of uploads which status was changed.
      */
     public int failInProgressUploads(UploadResult fail) {
         Log_OC.v(TAG, "Updating state of any killed upload");
@@ -584,16 +576,16 @@ public class UploadsStorageManager extends Observable {
         ContentValues cv = new ContentValues();
         cv.put(ProviderTableMeta.UPLOADS_STATUS, UploadStatus.UPLOAD_FAILED.getValue());
         cv.put(
-            ProviderTableMeta.UPLOADS_LAST_RESULT,
-            fail != null ? fail.getValue() : UploadResult.UNKNOWN.getValue()
+                ProviderTableMeta.UPLOADS_LAST_RESULT,
+                fail != null ? fail.getValue() : UploadResult.UNKNOWN.getValue()
         );
         cv.put(ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP, Calendar.getInstance().getTimeInMillis());
 
         int result = getDB().update(
-            ProviderTableMeta.CONTENT_URI_UPLOADS,
-            cv,
-            ProviderTableMeta.UPLOADS_STATUS + "=?",
-            new String[]{String.valueOf(UploadStatus.UPLOAD_IN_PROGRESS.getValue())}
+                ProviderTableMeta.CONTENT_URI_UPLOADS,
+                cv,
+                ProviderTableMeta.UPLOADS_STATUS + "=?",
+                new String[]{String.valueOf(UploadStatus.UPLOAD_IN_PROGRESS.getValue())}
         );
 
         if (result == 0) {
@@ -602,15 +594,7 @@ public class UploadsStorageManager extends Observable {
             Log_OC.w(TAG, Integer.toString(result) + " uploads where abruptly interrupted");
             notifyObserversNow();
         }
-        return result;
-    }
 
-    public int removeAccountUploads(Account account) {
-        Log_OC.v(TAG, "Delete all uploads for account " + account.name);
-        return getDB().delete(
-                ProviderTableMeta.CONTENT_URI_UPLOADS,
-                ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "=?",
-                new String[]{account.name});
+        return result;
     }
-
 }

+ 45 - 10
src/main/java/com/owncloud/android/db/OCUpload.java

@@ -40,7 +40,6 @@ import java.io.File;
 /**
  * Stores all information in order to start upload operations. PersistentUploadObject can
  * be stored persistently by {@link UploadsStorageManager}.
- *
  */
 public class OCUpload implements Parcelable {
 
@@ -49,7 +48,7 @@ public class OCUpload implements Parcelable {
     private long mId;
 
     /**
-     * Absolute path in the local file system to the file to be uploaded
+     * Absolute path in the local file system to the file to be uploaded.
      */
     private String mLocalPath;
 
@@ -64,7 +63,7 @@ public class OCUpload implements Parcelable {
     private String mAccountName;
 
     /**
-     * File size
+     * File size.
      */
     private long mFileSize;
 
@@ -77,14 +76,17 @@ public class OCUpload implements Parcelable {
      * Overwrite destination file?
      */
     private boolean mForceOverwrite;
+
     /**
      * Create destination folder?
      */
     private boolean mIsCreateRemoteFolder;
+
     /**
      * Status of upload (later, in_progress, ...).
      */
     private UploadStatus mUploadStatus;
+
     /**
      * Result from last upload operation. Can be null.
      */
@@ -95,14 +97,23 @@ public class OCUpload implements Parcelable {
      */
     private int mCreatedBy;
 
-    /*
+    /**
      * When the upload ended
      */
     private long mUploadEndTimeStamp;
 
+    /**
+     * Upload only via wifi?
+     */
+    private boolean mIsUseWifiOnly;
 
     /**
-     * Main constructor
+     * Upload only if phone being charged?
+     */
+    private boolean mIsWhileChargingOnly;
+
+     /**
+     * Main constructor.
      *
      * @param localPath         Absolute path in the local file system to the file to be uploaded.
      * @param remotePath        Absolute path in the remote account to set to the uploaded file.
@@ -124,9 +135,8 @@ public class OCUpload implements Parcelable {
         mAccountName = accountName;
     }
 
-
     /**
-     * Convenience constructor to reupload already existing {@link OCFile}s
+     * Convenience constructor to reupload already existing {@link OCFile}s.
      *
      * @param  ocFile           {@link OCFile} instance to update in the remote server.
      * @param  account          ownCloud {@link Account} where ocFile is contained.
@@ -135,7 +145,6 @@ public class OCUpload implements Parcelable {
         this(ocFile.getStoragePath(), ocFile.getRemotePath(), account.name);
     }
 
-
     /**
      * Reset all the fields to default values.
      */
@@ -151,6 +160,8 @@ public class OCUpload implements Parcelable {
         mUploadStatus = UploadStatus.UPLOAD_IN_PROGRESS;
         mLastResult = UploadResult.UNKNOWN;
         mCreatedBy = UploadFileOperation.CREATED_BY_USER;
+        mIsUseWifiOnly = true;
+        mIsWhileChargingOnly = false;
     }
 
     // Getters & Setters
@@ -230,7 +241,6 @@ public class OCUpload implements Parcelable {
         mFileSize = fileSize;
     }
 
-
     /**
      * @return the mimeType
      */
@@ -340,6 +350,28 @@ public class OCUpload implements Parcelable {
         }
     };
 
+    /**
+     * @return the isUseWifiOnly
+     */
+    public boolean isUseWifiOnly() {
+        return mIsUseWifiOnly;
+    }
+
+    /**
+     * @param isUseWifiOnly the isUseWifiOnly to set
+     */
+    public void setUseWifiOnly(boolean isUseWifiOnly) {
+        this.mIsUseWifiOnly = isUseWifiOnly;
+    }
+
+    public void setWhileChargingOnly(boolean isWhileChargingOnly) {
+        this.mIsWhileChargingOnly = isWhileChargingOnly;
+    }
+
+    public boolean isWhileChargingOnly() {
+        return mIsWhileChargingOnly;
+    }
+
     /**
      * Reconstruct from parcel
      *
@@ -369,9 +401,10 @@ public class OCUpload implements Parcelable {
             mLastResult = UploadResult.UNKNOWN;
         }
         mCreatedBy = source.readInt();
+        mIsUseWifiOnly = (source.readInt() == 1);
+        mIsWhileChargingOnly = (source.readInt() == 1);
     }
 
-
     @Override
     public int describeContents() {
         return this.hashCode();
@@ -390,6 +423,8 @@ public class OCUpload implements Parcelable {
         dest.writeLong(mUploadEndTimeStamp);
         dest.writeString(((mLastResult == null) ? "" : mLastResult.name()));
         dest.writeInt(mCreatedBy);
+        dest.writeInt(mIsUseWifiOnly ? 1 : 0);
+        dest.writeInt(mIsWhileChargingOnly ? 1 : 0);
     }
 
     enum CanUploadFileNowStatus {NOW, LATER, FILE_GONE, ERROR}

+ 30 - 8
src/main/java/com/owncloud/android/db/PreferenceManager.java

@@ -48,6 +48,8 @@ public abstract class PreferenceManager {
     private static final String PREF__LEGACY_CLEAN = "legacyClean";
     private static final String PREF__AUTO_UPLOAD_UPDATE_PATH = "autoUploadPathUpdate";
     private static final String PREF__PUSH_TOKEN = "pushToken";
+    private static final String PREF__AUTO_UPLOAD_SPLIT_OUT = "autoUploadEntriesSplitOut";
+    private static final String PREF__AUTO_UPLOAD_INIT = "autoUploadInit";
 
     public static void setPushToken(Context context, String pushToken) {
         saveStringPreferenceNow(context, PREF__PUSH_TOKEN, pushToken);
@@ -198,6 +200,10 @@ public abstract class PreferenceManager {
         saveBooleanPreference(context, AUTO_PREF__SORT_ASCENDING, ascending);
     }
 
+    public static boolean getAutoUploadInit(Context context) {
+        return getDefaultSharedPreferences(context).getBoolean(PREF__AUTO_UPLOAD_INIT, false);
+    }
+
     /**
      * Gets the legacy cleaning flag last set.
      *
@@ -218,6 +224,15 @@ public abstract class PreferenceManager {
         return getDefaultSharedPreferences(context).getBoolean(PREF__AUTO_UPLOAD_UPDATE_PATH, false);
     }
 
+    /**
+     * Gets the auto upload split out flag last set.
+     *
+     * @param context Caller {@link Context}, used to access to shared preferences manager.
+     * @return ascending order     the legacy cleaning flag, default is false
+     */
+    public static boolean getAutoUploadSplitEntries(Context context) {
+        return getDefaultSharedPreferences(context).getBoolean(PREF__AUTO_UPLOAD_SPLIT_OUT, false);
+    }
 
     /**
      * Saves the legacy cleaning flag which the user has set last.
@@ -229,6 +244,10 @@ public abstract class PreferenceManager {
         saveBooleanPreference(context, PREF__LEGACY_CLEAN, legacyClean);
     }
 
+    public static void setAutoUploadInit(Context context, boolean autoUploadInit) {
+        saveBooleanPreference(context, PREF__AUTO_UPLOAD_INIT, autoUploadInit);
+    }
+
     /**
      * Saves the legacy cleaning flag which the user has set last.
      *
@@ -239,6 +258,15 @@ public abstract class PreferenceManager {
         saveBooleanPreference(context, PREF__AUTO_UPLOAD_UPDATE_PATH, pathUpdate);
     }
 
+    /**
+     * Saves the flag for split entries magic
+     *
+     * @param context    Caller {@link Context}, used to access to shared preferences manager.
+     * @param splitOut flag if it is a auto upload path update
+     */
+    public static void setAutoUploadSplitEntries(Context context, boolean splitOut) {
+        saveBooleanPreference(context, PREF__AUTO_UPLOAD_SPLIT_OUT, splitOut);
+    }
 
     /**
      * Gets the uploader behavior which the user has set last.
@@ -280,7 +308,7 @@ public abstract class PreferenceManager {
         saveFloatPreference(context, AUTO_PREF__GRID_COLUMNS, gridColumns);
     }
 
-    public static void saveBooleanPreference(Context context, String key, boolean value) {
+    private static void saveBooleanPreference(Context context, String key, boolean value) {
         SharedPreferences.Editor appPreferences = getDefaultSharedPreferences(context.getApplicationContext()).edit();
         appPreferences.putBoolean(key, value).apply();
     }
@@ -301,17 +329,11 @@ public abstract class PreferenceManager {
         appPreferences.putInt(key, value).apply();
     }
 
-    public static void saveFloatPreference(Context context, String key, float value) {
+    private static void saveFloatPreference(Context context, String key, float value) {
         SharedPreferences.Editor appPreferences = getDefaultSharedPreferences(context.getApplicationContext()).edit();
         appPreferences.putFloat(key, value).apply();
     }
 
-    private static void saveLongPreference(Context context, String key, long value) {
-        SharedPreferences.Editor appPreferences = getDefaultSharedPreferences(context.getApplicationContext()).edit();
-        appPreferences.putLong(key, value);
-        appPreferences.apply();
-    }
-
     public static SharedPreferences getDefaultSharedPreferences(Context context) {
         return android.preference.PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
     }

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

@@ -32,7 +32,7 @@ import com.owncloud.android.MainApp;
 public class ProviderMeta {
 
     public static final String DB_NAME = "filelist";
-    public static final int DB_VERSION = 22;
+    public static final int DB_VERSION = 23;
 
     private ProviderMeta() {
     }
@@ -46,6 +46,7 @@ public class ProviderMeta {
         public static final String EXTERNAL_LINKS_TABLE_NAME = "external_links";
         public static final String ARBITRARY_DATA_TABLE_NAME = "arbitrary_data";
         public static final String VIRTUAL_TABLE_NAME = "virtual";
+        public static final String FILESYSTEM_TABLE_NAME = "filesystem";
 
         private static final String CONTENT_PREFIX = "content://";
 
@@ -68,6 +69,9 @@ public class ProviderMeta {
         public static final Uri CONTENT_URI_ARBITRARY_DATA = Uri.parse(CONTENT_PREFIX
                 + MainApp.getAuthority() + "/arbitrary_data");
         public static final Uri CONTENT_URI_VIRTUAL = Uri.parse(CONTENT_PREFIX + MainApp.getAuthority() + "/virtual");
+        public static final Uri CONTENT_URI_FILESYSTEM = Uri.parse(CONTENT_PREFIX
+                + MainApp.getAuthority() + "/filesystem");
+
 
         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";
@@ -169,6 +173,8 @@ public class ProviderMeta {
         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";
+        public static final String UPLOADS_IS_WHILE_CHARGING_ONLY = "is_while_charging_only";
+        public static final String UPLOADS_IS_WIFI_ONLY = "is_wifi_only";
 
         // Columns of synced folder table
         public static final String SYNCED_FOLDER_LOCAL_PATH = "local_path";
@@ -176,6 +182,7 @@ public class ProviderMeta {
         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_TYPE = "type";
         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";
@@ -192,8 +199,17 @@ public class ProviderMeta {
         public static final String ARBITRARY_DATA_KEY = "key";
         public static final String ARBITRARY_DATA_VALUE = "value";
 
+
         // Columns of virtual
         public static final String VIRTUAL_TYPE = "type";
         public static final String VIRTUAL_OCFILE_ID = "ocfile_id";
+
+        // Columns of filesystem data table
+        public static final String FILESYSTEM_FILE_LOCAL_PATH = "local_path";
+        public static final String FILESYSTEM_FILE_MODIFIED = "modified_at";
+        public static final String FILESYSTEM_FILE_IS_FOLDER = "is_folder";
+        public static final String FILESYSTEM_FILE_FOUND_RECENTLY = "found_at";
+        public static final String FILESYSTEM_FILE_SENT_FOR_UPLOAD = "upload_triggered";
+        public static final String FILESYSTEM_SYNCED_FOLDER_ID = "syncedfolder_id";
     }
 }

+ 6 - 1
src/main/java/com/owncloud/android/db/UploadResult.java

@@ -35,7 +35,8 @@ public enum UploadResult {
     DELAYED_FOR_WIFI(9),
     SERVICE_INTERRUPTED(10),
     DELAYED_FOR_CHARGING(11),
-    MAINTENANCE_MODE(12);
+    MAINTENANCE_MODE(12),
+    LOCK_FAILED(13);
 
     private final int value;
 
@@ -77,6 +78,8 @@ public enum UploadResult {
                 return DELAYED_FOR_CHARGING;
             case 12:
                 return MAINTENANCE_MODE;
+            case 13:
+                return LOCK_FAILED;
         }
         return null;
     }
@@ -120,6 +123,8 @@ public enum UploadResult {
                 return UNKNOWN;
             case MAINTENANCE_MODE:
                 return MAINTENANCE_MODE;
+            case LOCK_FAILED:
+                return LOCK_FAILED;
             default:
                 return UNKNOWN;
         }

+ 0 - 223
src/main/java/com/owncloud/android/files/InstantUploadBroadcastReceiver.java

@@ -1,223 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author Bartek Przybylski
- * @author David A. Velasco
- * Copyright (C) 2012  Bartek Przybylski
- * Copyright (C) 2016 ownCloud Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.owncloud.android.files;
-
-import android.Manifest;
-import android.accounts.Account;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.database.Cursor;
-import android.os.Build;
-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;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.operations.UploadFileOperation;
-import com.owncloud.android.utils.FileStorageUtils;
-
-
-public class InstantUploadBroadcastReceiver extends BroadcastReceiver {
-
-    private static final String TAG = InstantUploadBroadcastReceiver.class.getName();
-    // Image action
-    // Unofficial action, works for most devices but not HTC. See: https://github.com/owncloud/android/issues/6
-    private static final String NEW_PHOTO_ACTION_UNOFFICIAL = "com.android.camera.NEW_PICTURE";
-    // Officially supported action since SDK 14:
-    // http://developer.android.com/reference/android/hardware/Camera.html#ACTION_NEW_PICTURE
-    private static final String NEW_PHOTO_ACTION = "android.hardware.action.NEW_PICTURE";
-    // Video action
-    // Officially supported action since SDK 14:
-    // http://developer.android.com/reference/android/hardware/Camera.html#ACTION_NEW_VIDEO
-    private static final String NEW_VIDEO_ACTION = "android.hardware.action.NEW_VIDEO";
-
-    /**
-     * Because we support NEW_PHOTO_ACTION and NEW_PHOTO_ACTION_UNOFFICIAL it can happen that
-     * handleNewPictureAction is called twice for the same photo. Use this simple static variable to
-     * remember last uploaded photo to filter duplicates. Must not be null!
-     */
-    static String lastUploadedPhotoPath = "";
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
-            Log_OC.d(TAG, "Received: " + intent.getAction());
-            if (intent.getAction().equals(NEW_PHOTO_ACTION_UNOFFICIAL)) {
-                handleNewPictureAction(context, intent);
-                Log_OC.d(TAG, "UNOFFICIAL processed: com.android.camera.NEW_PICTURE");
-            } else if (intent.getAction().equals(NEW_PHOTO_ACTION)) {
-                handleNewPictureAction(context, intent);
-                Log_OC.d(TAG, "OFFICIAL processed: android.hardware.action.NEW_PICTURE");
-            } else if (intent.getAction().equals(NEW_VIDEO_ACTION)) {
-                handleNewVideoAction(context, intent);
-                Log_OC.d(TAG, "OFFICIAL processed: android.hardware.action.NEW_VIDEO");
-            } else {
-                Log_OC.e(TAG, "Incorrect intent received: " + intent.getAction());
-            }
-        }
-    }
-
-    private void handleNewPictureAction(Context context, Intent intent) {
-        Cursor c = null;
-        String file_path = null;
-        String file_name = null;
-        String mime_type = null;
-        long date_taken = 0;
-
-        Log_OC.i(TAG, "New photo received");
-
-        if (!PreferenceManager.instantPictureUploadEnabled(context)) {
-            Log_OC.d(TAG, "Instant picture upload disabled, ignoring new picture");
-            return;
-        }
-
-        Account account = AccountUtils.getCurrentOwnCloudAccount(context);
-        if (account == null) {
-            Log_OC.w(TAG, "No account found for instant upload, aborting");
-            return;
-        }
-
-        String[] CONTENT_PROJECTION = {
-                Images.Media.DATA, Images.Media.DISPLAY_NAME, Images.Media.MIME_TYPE, Images.Media.SIZE};
-
-        // if < Jelly Bean permission must be accepted during installation
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
-            int permissionCheck = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE);
-
-            if (android.content.pm.PackageManager.PERMISSION_GRANTED != permissionCheck) {
-                Log_OC.w(TAG, "Read external storage permission isn't granted, aborting");
-                return;
-            }
-        }
-
-        c = context.getContentResolver().query(intent.getData(), CONTENT_PROJECTION, null, null, null);
-        if (!c.moveToFirst()) {
-            Log_OC.e(TAG, "Couldn't resolve given uri: " + intent.getDataString());
-            return;
-        }
-        file_path = c.getString(c.getColumnIndex(Images.Media.DATA));
-        file_name = c.getString(c.getColumnIndex(Images.Media.DISPLAY_NAME));
-        mime_type = c.getString(c.getColumnIndex(Images.Media.MIME_TYPE));
-        date_taken = System.currentTimeMillis();
-        c.close();
-
-        if (file_path.equals(lastUploadedPhotoPath)) {
-            Log_OC.d(TAG, "Duplicate detected: " + file_path + ". Ignore.");
-            return;
-        }
-
-        lastUploadedPhotoPath = file_path;
-        Log_OC.d(TAG, "Path: " + file_path + "");
-
-        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(uploadPath, file_name, date_taken, subfolderByDate),
-                behaviour,
-                mime_type,
-                true,           // create parent folder if not existent
-                UploadFileOperation.CREATED_AS_INSTANT_PICTURE
-        );
-    }
-
-    private Integer getUploadBehaviour(Context context) {
-        SharedPreferences appPreferences = android.preference.PreferenceManager.getDefaultSharedPreferences(context);
-        String behaviour = appPreferences.getString("prefs_instant_behaviour", "NOTHING");
-
-        if (behaviour.equalsIgnoreCase("NOTHING")) {
-            Log_OC.d(TAG, "upload file and do nothing");
-            return FileUploader.LOCAL_BEHAVIOUR_FORGET;
-        } else if (behaviour.equalsIgnoreCase("MOVE")) {
-            Log_OC.d(TAG, "upload file and move file to oc folder");
-            return FileUploader.LOCAL_BEHAVIOUR_MOVE;
-        } else if (behaviour.equalsIgnoreCase("DELETE")) {
-            Log_OC.d(TAG, "upload file and delete original file");
-            return FileUploader.LOCAL_BEHAVIOUR_DELETE;
-        }
-        return FileUploader.LOCAL_BEHAVIOUR_FORGET;
-    }
-
-    private void handleNewVideoAction(Context context, Intent intent) {
-        Cursor c = null;
-        String file_path = null;
-        String file_name = null;
-        String mime_type = null;
-        long date_taken = 0;
-
-        Log_OC.i(TAG, "New video received");
-
-        if (!PreferenceManager.instantVideoUploadEnabled(context)) {
-            Log_OC.d(TAG, "Instant video upload disabled, ignoring new video");
-            return;
-        }
-
-        Account account = AccountUtils.getCurrentOwnCloudAccount(context);
-        if (account == null) {
-            Log_OC.w(TAG, "No account found for instant upload, aborting");
-            return;
-        }
-
-        String[] CONTENT_PROJECTION = {Video.Media.DATA, Video.Media.DISPLAY_NAME, Video.Media.MIME_TYPE,
-                Video.Media.SIZE};
-        c = context.getContentResolver().query(intent.getData(), CONTENT_PROJECTION, null, null, null);
-        if (!c.moveToFirst()) {
-            Log_OC.e(TAG, "Couldn't resolve given uri: " + intent.getDataString());
-            return;
-        }
-        file_path = c.getString(c.getColumnIndex(Video.Media.DATA));
-        file_name = c.getString(c.getColumnIndex(Video.Media.DISPLAY_NAME));
-        mime_type = c.getString(c.getColumnIndex(Video.Media.MIME_TYPE));
-        c.close();
-        date_taken = System.currentTimeMillis();
-        Log_OC.d(TAG, file_path + "");
-
-        int behaviour = getUploadBehaviour(context);
-        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-        requester.uploadNewFile(
-                context,
-                account,
-                file_path,
-                FileStorageUtils.getInstantVideoUploadFilePath(context, file_name, date_taken),
-                behaviour,
-                mime_type,
-                true,           // create parent folder if not existent
-                UploadFileOperation.CREATED_AS_INSTANT_VIDEO
-        );
-    }
-
-}

+ 0 - 223
src/main/java/com/owncloud/android/files/services/ConnectivityActionReceiver.java

@@ -1,223 +0,0 @@
-/**
- *   ownCloud Android client application
- *
- *   @author LukeOwncloud
- *   Copyright (C) 2016 ownCloud Inc.
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License version 2,
- *   as published by the Free Software Foundation.
- *
- *   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 General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-package com.owncloud.android.files.services;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.net.NetworkInfo;
-import android.net.wifi.WifiInfo;
-import android.net.wifi.WifiManager;
-import android.os.Bundle;
-
-import com.owncloud.android.db.PreferenceManager;
-import com.owncloud.android.db.UploadResult;
-import com.owncloud.android.lib.common.utils.Log_OC;
-
-/**
- * Receives all connectivity action from Android OS at all times and performs
- * required OC actions. For now that are: - Signal connectivity to
- * {@link FileUploader}.
- * 
- * Later can be added: - Signal connectivity to download service, deletion
- * service, ... - Handle offline mode (cf.
- * https://github.com/owncloud/android/issues/162)
- *
- * Have fun with the comments :S
- */
-public class ConnectivityActionReceiver extends BroadcastReceiver {
-    private static final String TAG = ConnectivityActionReceiver.class.getSimpleName();
-
-    /**
-     * Magic keyword, by Google.
-     *
-     * {@see http://developer.android.com/intl/es/reference/android/net/wifi/WifiInfo.html#getSSID()}
-     */
-    private static final String UNKNOWN_SSID = "<unknown ssid>";
-
-
-    @Override
-    public void onReceive(final Context context, Intent intent) {
-        // LOG ALL EVENTS:
-        Log_OC.v(TAG, "action: " + intent.getAction());
-        Log_OC.v(TAG, "component: " + intent.getComponent());
-        Bundle extras = intent.getExtras();
-        if (extras != null) {
-            for (String key : extras.keySet()) {
-                Log_OC.v(TAG, "key [" + key + "]: " + extras.get(key));
-            }
-        } else {
-            Log_OC.v(TAG, "no extras");
-        }
-
-        if (intent.getAction().equals(Intent.ACTION_POWER_CONNECTED) &&
-                (PreferenceManager.instantPictureUploadEnabled(context) &&
-                        PreferenceManager.instantPictureUploadWhenChargingOnly(context)) ||
-                (PreferenceManager.instantVideoUploadEnabled(context) &&
-                        PreferenceManager.instantVideoUploadWhenChargingOnly(context))
-                ) {
-            // for the moment, only recovery of instant uploads, similar to behaviour in release 1.9.1
-            Log_OC.d(TAG, "Requesting retry of instant uploads (& friends) due to charging");
-            FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-            requester.retryFailedUploads(
-                    context,
-                    null,
-                    UploadResult.DELAYED_FOR_CHARGING   // for the rest of enqueued when Wifi fell
-            );
-        }
-
-        /**
-         * There is an interesting mess to process WifiManager.NETWORK_STATE_CHANGED_ACTION and
-         * ConnectivityManager.CONNECTIVITY_ACTION in a simple and reliable way.
-         *
-         * The former triggers much more events than what we really need to know about Wifi connection.
-         *
-         * But there are annoying uncertainties about ConnectivityManager.CONNECTIVITY_ACTION due
-         * to the deprecation of ConnectivityManager.EXTRA_NETWORK_INFO in API level 14, and the absence
-         * of ConnectivityManager.EXTRA_NETWORK_TYPE until API level 17. Dear Google, how should we
-         * handle API levels 14 to 16?
-         *
-         * In the end maybe we need to keep in memory the current knowledge about connectivity
-         * and update it taking into account several Intents received in a row
-         *
-         * But first let's try something "simple" to keep a basic retry of instant uploads in
-         * version 1.9.2, similar to the existent until 1.9.1. To be improved.
-         */
-        if(intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
-            NetworkInfo networkInfo =
-                intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
-            WifiInfo wifiInfo =
-                intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO);
-            String bssid =
-                intent.getStringExtra(WifiManager.EXTRA_BSSID);
-            if(networkInfo.isConnected()   &&      // not enough; see (*) right below
-                wifiInfo != null    &&
-                !UNKNOWN_SSID.equals(wifiInfo.getSSID().toLowerCase()) &&
-                bssid != null
-                    ) {
-                Log_OC.d(TAG, "WiFi connected");
-
-                wifiConnected(context);
-            } else {
-                // TODO tons of things to check to conclude disconnection;
-                // TODO maybe alternative commented below, based on CONNECTIVITY_ACTION is better
-                Log_OC.d(TAG, "WiFi disconnected ... but don't know if right now");
-            }
-         }
-        // (*) When WiFi is lost, an Intent with network state CONNECTED and SSID "<unknown ssid>" is
-        //      received right before another Intent with network state DISCONNECTED; needs to
-        //      be differentiated of a new Wifi connection.
-        //
-        //  Besides, with a new connection two Intents are received, having only the second the extra
-        //  WifiManager.EXTRA_BSSID, with the BSSID of the access point accessed.
-        //
-        //  Not sure if this protocol is exact, since it's not documented. Only found mild references in
-        //   - http://developer.android.com/intl/es/reference/android/net/wifi/WifiInfo.html#getSSID()
-        //   - http://developer.android.com/intl/es/reference/android/net/wifi/WifiManager.html#EXTRA_BSSID
-        //  and reproduced in Nexus 5 with Android 6.
-
-
-        /**
-         * Possible alternative attending ConnectivityManager.CONNECTIVITY_ACTION.
-         *
-         * Let's see what QA has to say
-         *
-        if(intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
-            NetworkInfo networkInfo = intent.getParcelableExtra(
-                    ConnectivityManager.EXTRA_NETWORK_INFO      // deprecated in API 14
-            );
-            int networkType = intent.getIntExtra(
-                    ConnectivityManager.EXTRA_NETWORK_TYPE,     // only from API level 17
-                    -1
-            );
-            boolean couldBeWifiAction =
-                    (networkInfo == null && networkType < 0)    ||      // cases of lack of info
-                    networkInfo.getType() == ConnectivityManager.TYPE_WIFI  ||
-                    networkType == ConnectivityManager.TYPE_WIFI;
-
-            if (couldBeWifiAction) {
-                if (ConnectivityUtils.isAppConnectedViaUnmeteredWiFi(context)) {
-                    Log_OC.d(TAG, "WiFi connected");
-                    wifiConnected(context);
-                } else {
-                    Log_OC.d(TAG, "WiFi disconnected");
-                    wifiDisconnected(context);
-                }
-            } /* else, CONNECTIVIY_ACTION is (probably) about other network interface (mobile, bluetooth, ...)
-        }
-        */
-    }
-
-    private void wifiConnected(Context context) {
-        // for the moment, only recovery of instant uploads, similar to behaviour in release 1.9.1
-        if (
-                (PreferenceManager.instantPictureUploadEnabled(context) &&
-                        PreferenceManager.instantPictureUploadViaWiFiOnly(context)) ||
-                (PreferenceManager.instantVideoUploadEnabled(context) &&
-                        PreferenceManager.instantVideoUploadViaWiFiOnly(context))
-                ) {
-            Log_OC.d(TAG, "Requesting retry of instant uploads (& friends)");
-            FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-            requester.retryFailedUploads(
-                context,
-                null,
-                UploadResult.NETWORK_CONNECTION     // for the interrupted when Wifi fell, if any
-                // (side effect: any upload failed due to network error will be retried too, instant or not)
-            );
-            requester.retryFailedUploads(
-                context,
-                null,
-                UploadResult.DELAYED_FOR_WIFI       // for the rest of enqueued when Wifi fell
-            );
-        }
-    }
-
-    /**
-     *
-    private void wifiDisconnected() {
-        // TODO something smart
-
-        // NOTE: explicit cancellation of only-wifi instant uploads is not needed anymore, since currently:
-        //  - any upload in progress will be interrupted due to the lack of connectivity while the device
-        //      reconnects through other network interface;
-        //  - FileUploader checks instant upload settings and connection state before executing each
-        //    upload operation, so other pending instant uploads after the current one will not be run
-        //    (currently are silently moved to FAILED state)
-    }
-
-
-
-    static public void enableActionReceiver(Context context) {
-        PackageManager pm = context.getPackageManager();
-        ComponentName compName = new ComponentName(context.getApplicationContext(), ConnectivityActionReceiver.class);
-        pm.setComponentEnabledSetting(compName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
-                PackageManager.DONT_KILL_APP);
-    }
-
-    static public void disableActionReceiver(Context context) {
-        PackageManager pm = context.getPackageManager();
-        ComponentName compName = new ComponentName(context.getApplicationContext(), ConnectivityActionReceiver.class);
-        pm.setComponentEnabledSetting(compName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
-                PackageManager.DONT_KILL_APP);
-    }
-
-    */
-}

+ 123 - 50
src/main/java/com/owncloud/android/files/services/FileUploader.java

@@ -34,7 +34,6 @@ import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Binder;
-import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
@@ -45,6 +44,9 @@ import android.os.Process;
 import android.support.v4.app.NotificationCompat;
 import android.util.Pair;
 
+import com.evernote.android.job.JobRequest;
+import com.evernote.android.job.util.Device;
+import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.authentication.AuthenticatorActivity;
@@ -77,6 +79,8 @@ import java.util.Iterator;
 import java.util.Map;
 import java.util.Vector;
 
+import javax.annotation.Nullable;
+
 /**
  * Service for uploading files. Invoke using context.startService(...).
  *
@@ -144,6 +148,9 @@ public class FileUploader extends Service
      * Key to signal what is the origin of the upload request
      */
     public static final String KEY_CREATED_BY = "CREATED_BY";
+
+    public static final String KEY_WHILE_ON_WIFI_ONLY = "KEY_ON_WIFI_ONLY";
+
     /**
      * Set to true if upload is to performed only when phone is being charged.
      */
@@ -194,7 +201,6 @@ public class FileUploader extends Service
         sendBroadcastUploadStarted(mCurrentUpload);
     }
 
-
     /**
      * Helper class providing methods to ease requesting commands to {@link FileUploader} .
      *
@@ -214,7 +220,37 @@ public class FileUploader extends Service
                 String[] mimeTypes,
                 Integer behaviour,
                 Boolean createRemoteFolder,
-                int createdBy
+                int createdBy,
+                boolean requiresWifi,
+                boolean requiresCharging
+        ) {
+            Intent intent = new Intent(context, FileUploader.class);
+
+            intent.putExtra(FileUploader.KEY_ACCOUNT, account);
+            intent.putExtra(FileUploader.KEY_LOCAL_FILE, localPaths);
+            intent.putExtra(FileUploader.KEY_REMOTE_FILE, remotePaths);
+            intent.putExtra(FileUploader.KEY_MIME_TYPE, mimeTypes);
+            intent.putExtra(FileUploader.KEY_LOCAL_BEHAVIOUR, behaviour);
+            intent.putExtra(FileUploader.KEY_CREATE_REMOTE_FOLDER, createRemoteFolder);
+            intent.putExtra(FileUploader.KEY_CREATED_BY, createdBy);
+            intent.putExtra(FileUploader.KEY_WHILE_ON_WIFI_ONLY, requiresWifi);
+            intent.putExtra(FileUploader.KEY_WHILE_CHARGING_ONLY, requiresCharging);
+
+            context.startService(intent);
+        }
+
+        public void uploadFileWithOverwrite(
+                Context context,
+                Account account,
+                String[] localPaths,
+                String[] remotePaths,
+                String[] mimeTypes,
+                Integer behaviour,
+                Boolean createRemoteFolder,
+                int createdBy,
+                boolean requiresWifi,
+                boolean requiresCharging,
+                boolean overwrite
         ) {
             Intent intent = new Intent(context, FileUploader.class);
 
@@ -225,15 +261,41 @@ public class FileUploader extends Service
             intent.putExtra(FileUploader.KEY_LOCAL_BEHAVIOUR, behaviour);
             intent.putExtra(FileUploader.KEY_CREATE_REMOTE_FOLDER, createRemoteFolder);
             intent.putExtra(FileUploader.KEY_CREATED_BY, createdBy);
+            intent.putExtra(FileUploader.KEY_WHILE_ON_WIFI_ONLY, requiresWifi);
+            intent.putExtra(FileUploader.KEY_WHILE_CHARGING_ONLY, requiresCharging);
+            intent.putExtra(FileUploader.KEY_FORCE_OVERWRITE, overwrite);
 
             context.startService(intent);
         }
 
+        /**
+         * Call to upload a file
+         */
+        public void uploadFileWithOverwrite(Context context, Account account, String localPath, String remotePath, int
+                behaviour, String mimeType, boolean createRemoteFile, int createdBy, boolean requiresWifi,
+                                  boolean requiresCharging, boolean overwrite) {
+
+            uploadFileWithOverwrite(
+                    context,
+                    account,
+                    new String[]{localPath},
+                    new String[]{remotePath},
+                    new String[]{mimeType},
+                    behaviour,
+                    createRemoteFile,
+                    createdBy,
+                    requiresWifi,
+                    requiresCharging,
+                    overwrite
+            );
+        }
+
         /**
          * Call to upload a new single file
          */
         public void uploadNewFile(Context context, Account account, String localPath, String remotePath, int
-                behaviour, String mimeType, boolean createRemoteFile, int createdBy) {
+                behaviour, String mimeType, boolean createRemoteFile, int createdBy, boolean requiresWifi,
+                                  boolean requiresCharging) {
 
             uploadNewFile(
                 context,
@@ -243,7 +305,9 @@ public class FileUploader extends Service
                 new String[]{mimeType},
                 behaviour,
                 createRemoteFile,
-                createdBy
+                createdBy,
+                requiresWifi,
+                requiresCharging
             );
         }
 
@@ -288,7 +352,6 @@ public class FileUploader extends Service
             }
         }
 
-
         /**
          * Retry a subset of all the stored failed uploads.
          *
@@ -306,7 +369,7 @@ public class FileUploader extends Service
             boolean accountMatch;
             for ( OCUpload failedUpload: failedUploads) {
                 accountMatch = (account == null || account.name.equals(failedUpload.getAccountName()));
-                resultMatch = (uploadResult == null || uploadResult.equals(failedUpload.getLastResult()));
+                resultMatch = ((uploadResult == null || uploadResult.equals(failedUpload.getLastResult())));
                 if (accountMatch && resultMatch) {
                     if (currentAccount == null ||
                             !currentAccount.name.equals(failedUpload.getAccountName())) {
@@ -330,13 +393,12 @@ public class FileUploader extends Service
                 i.putExtra(FileUploader.KEY_RETRY, true);
                 i.putExtra(FileUploader.KEY_ACCOUNT, account);
                 i.putExtra(FileUploader.KEY_RETRY_UPLOAD, upload);
+
                 context.startService(i);
             }
         }
-
     }
 
-
     /**
      * Service initialization
      */
@@ -370,7 +432,6 @@ public class FileUploader extends Service
         am.addOnAccountsUpdatedListener(this, null, false);
     }
 
-
     /**
      * Service clean-up when restarted after being killed
      */
@@ -379,7 +440,6 @@ public class FileUploader extends Service
         mNotificationManager.cancel(R.string.uploader_upload_in_progress_ticker);
     }
 
-
     /**
      * Service clean up
      */
@@ -399,7 +459,6 @@ public class FileUploader extends Service
         super.onDestroy();
     }
 
-
     /**
      * Entry point to add one or several files to the queue of uploads.
      *
@@ -426,9 +485,14 @@ public class FileUploader extends Service
             return Service.START_NOT_STICKY;
         }
         OwnCloudVersion ocv = AccountUtils.getServerVersion(account);
+
         boolean chunked = ocv.isChunkedUploadSupported();
 
+        boolean onWifiOnly = intent.getBooleanExtra(KEY_WHILE_ON_WIFI_ONLY, false);
+        boolean whileChargingOnly = intent.getBooleanExtra(KEY_WHILE_CHARGING_ONLY, false);
+
         if (!retry) {
+
             if (!(intent.hasExtra(KEY_LOCAL_FILE) ||
                     intent.hasExtra(KEY_FILE))) {
                 Log_OC.e(TAG, "Not enough information provided in intent");
@@ -451,7 +515,6 @@ public class FileUploader extends Service
                 mimeTypes = intent.getStringArrayExtra(KEY_MIME_TYPE);
             }
 
-
             boolean forceOverwrite = intent.getBooleanExtra(KEY_FORCE_OVERWRITE, false);
             int localAction = intent.getIntExtra(KEY_LOCAL_BEHAVIOUR, LOCAL_BEHAVIOUR_FORGET);
 
@@ -503,10 +566,12 @@ public class FileUploader extends Service
                     ocUpload.setCreateRemoteFolder(isCreateRemoteFolder);
                     ocUpload.setCreatedBy(createdBy);
                     ocUpload.setLocalAction(localAction);
-                    /*ocUpload.setUseWifiOnly(isUseWifiOnly);
-                    ocUpload.setWhileChargingOnly(isWhileChargingOnly);*/
+                    ocUpload.setUseWifiOnly(onWifiOnly);
+                    ocUpload.setWhileChargingOnly(whileChargingOnly);
+
                     ocUpload.setUploadStatus(UploadStatus.UPLOAD_IN_PROGRESS);
 
+
                     newUpload = new UploadFileOperation(
                             account,
                             files[i],
@@ -514,7 +579,9 @@ public class FileUploader extends Service
                             chunked,
                             forceOverwrite,
                             localAction,
-                            this
+                            this,
+                            onWifiOnly,
+                            whileChargingOnly
                     );
                     newUpload.setCreatedBy(createdBy);
                     if (isCreateRemoteFolder) {
@@ -561,6 +628,9 @@ public class FileUploader extends Service
             }
             OCUpload upload = intent.getParcelableExtra(KEY_RETRY_UPLOAD);
 
+            onWifiOnly = upload.isUseWifiOnly();
+            whileChargingOnly = upload.isWhileChargingOnly();
+
             UploadFileOperation newUpload = new UploadFileOperation(
                     account,
                     null,
@@ -568,7 +638,9 @@ public class FileUploader extends Service
                     chunked,
                     upload.isForceOverwrite(),  // TODO should be read from DB?
                     upload.getLocalAction(),    // TODO should be read from DB?
-                    this
+                    this,
+                    onWifiOnly,
+                    whileChargingOnly
             );
 
             newUpload.addDatatransferProgressListener(this);
@@ -634,9 +706,8 @@ public class FileUploader extends Service
     }
 
     /**
-     * Binder to let client components to perform operations on the queue of
-     * uploads.
-     * <p/>
+     * Binder to let client components to perform operations on the queue of uploads.
+     *
      * It provides by itself the available operations.
      */
     public class FileUploaderBinder extends Binder implements OnDatatransferProgressListener {
@@ -645,9 +716,7 @@ public class FileUploader extends Service
          * Map of listeners that will be reported about progress of uploads from a
          * {@link FileUploaderBinder} instance
          */
-        private Map<String, OnDatatransferProgressListener> mBoundListeners =
-                new HashMap<String, OnDatatransferProgressListener>();
-
+        private Map<String, OnDatatransferProgressListener> mBoundListeners = new HashMap<>();
 
         /**
          * Cancels a pending or current upload of a remote file.
@@ -656,7 +725,7 @@ public class FileUploader extends Service
          * @param file    A file in the queue of pending uploads
          */
         public void cancel(Account account, OCFile file) {
-            cancel(account.name, file.getRemotePath());
+            cancel(account.name, file.getRemotePath(), null);
         }
 
         /**
@@ -665,7 +734,7 @@ public class FileUploader extends Service
          * @param storedUpload Upload operation persisted
          */
         public void cancel(OCUpload storedUpload) {
-            cancel(storedUpload.getAccountName(), storedUpload.getRemotePath());
+            cancel(storedUpload.getAccountName(), storedUpload.getRemotePath(), null);
 
         }
 
@@ -674,8 +743,10 @@ public class FileUploader extends Service
          *
          * @param accountName Local name of an ownCloud account where the remote file will be stored.
          * @param remotePath  Remote target of the upload
+         *
+         * Setting result code will pause rather than cancel the job
          */
-        private void cancel(String accountName, String remotePath) {
+        private void cancel(String accountName, String remotePath, @Nullable ResultCode resultCode ) {
             Pair<UploadFileOperation, String> removeResult =
                     mPendingUploads.remove(accountName, remotePath);
             UploadFileOperation upload = removeResult.first;
@@ -686,14 +757,17 @@ public class FileUploader extends Service
 
                 upload = mCurrentUpload;
             }
+
             if (upload != null) {
                 upload.cancel();
                 // need to update now table in mUploadsStorageManager,
                 // since the operation will not get to be run by FileUploader#uploadFile
-                mUploadsStorageManager.removeUpload(accountName, remotePath);
-            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-                // try to cancel job in jobScheduler
-                mUploadsStorageManager.cancelPendingJob(accountName, remotePath);
+                if (resultCode != null) {
+                    mUploadsStorageManager.updateDatabaseUploadResult(new RemoteOperationResult(resultCode), upload);
+                    notifyUploadResult(upload, new RemoteOperationResult(resultCode));
+                } else {
+                    mUploadsStorageManager.removeUpload(accountName, remotePath);
+                }
             }
         }
 
@@ -740,7 +814,6 @@ public class FileUploader extends Service
             return (mPendingUploads.contains(account.name, file.getRemotePath()));
         }
 
-
         public boolean isUploadingNow(OCUpload upload) {
             return (
                 upload != null  &&
@@ -751,7 +824,6 @@ public class FileUploader extends Service
             );
         }
 
-
         /**
          * Adds a listener interested in the progress of the upload for a concrete file.
          *
@@ -771,7 +843,6 @@ public class FileUploader extends Service
             mBoundListeners.put(targetKey, listener);
         }
 
-
         /**
          * Adds a listener interested in the progress of the upload for a concrete file.
          *
@@ -789,7 +860,6 @@ public class FileUploader extends Service
             mBoundListeners.put(targetKey, listener);
         }
 
-
         /**
          * Removes a listener interested in the progress of the upload for a concrete file.
          *
@@ -811,7 +881,6 @@ public class FileUploader extends Service
             }
         }
 
-
         /**
          * Removes a listener interested in the progress of the upload for a concrete file.
          *
@@ -831,7 +900,6 @@ public class FileUploader extends Service
             }
         }
 
-
         @Override
         public void onTransferProgress(long progressRate, long totalTransferredSoFar,
                                        long totalToTransfer, String fileName) {
@@ -840,12 +908,24 @@ public class FileUploader extends Service
             if (boundListener != null) {
                 boundListener.onTransferProgress(progressRate, totalTransferredSoFar,
                         totalToTransfer, fileName);
+
+                if (MainApp.getAppContext() != null) {
+                    if (mCurrentUpload.getIsWifiRequired() && !Device.getNetworkType(MainApp.getAppContext()).
+                            equals(JobRequest.NetworkType.UNMETERED)) {
+                        cancel(mCurrentUpload.getAccount().name, mCurrentUpload.getFile().getRemotePath()
+                                , ResultCode.DELAYED_FOR_WIFI);
+                    } else if (mCurrentUpload.getIsChargingRequired() &&
+                            !Device.isCharging(MainApp.getAppContext())) {
+                        cancel(mCurrentUpload.getAccount().name, mCurrentUpload.getFile().getRemotePath()
+                                , ResultCode.DELAYED_FOR_CHARGING);
+                    }
+                }
             }
         }
 
         /**
          * Builds a key for the map of listeners.
-         * <p/>
+         *
          * TODO use method in IndexedForest, or refactor both to a common place
          * add to local database) to better policy (add to local database, then upload)
          *
@@ -863,7 +943,7 @@ public class FileUploader extends Service
     /**
      * Upload worker. Performs the pending uploads in the order they were
      * requested.
-     * <p/>
+     *
      * Created with the Looper of a new thread, started in
      * {@link FileUploader#onCreate()}.
      */
@@ -958,7 +1038,7 @@ public class FileUploader extends Service
                             mCurrentAccount.name,
                             mCurrentUpload.getOldFile().getRemotePath()
                     );
-                    /** TODO: grant that name is also updated for mCurrentUpload.getOCUploadId */
+                    // TODO: grant that name is also updated for mCurrentUpload.getOCUploadId
 
                 } else {
                     removeResult = mPendingUploads.removePayload(
@@ -973,7 +1053,6 @@ public class FileUploader extends Service
                 notifyUploadResult(mCurrentUpload, uploadResult);
 
                 sendBroadcastUploadFinished(mCurrentUpload, uploadResult, removeResult.second);
-
             }
 
             // generate new Thumbnail
@@ -997,8 +1076,7 @@ public class FileUploader extends Service
     private void notifyUploadStart(UploadFileOperation upload) {
         // / create status notification with a progress bar
         mLastPercent = 0;
-        mNotificationBuilder =
-                NotificationUtils.newNotificationBuilder(this);
+        mNotificationBuilder = NotificationUtils.newNotificationBuilder(this);
         mNotificationBuilder
                 .setOngoing(true)
                 .setSmallIcon(R.drawable.notification_icon)
@@ -1022,7 +1100,6 @@ public class FileUploader extends Service
         }   // else wait until the upload really start (onTransferProgress is called), so that if it's discarded
         // due to lack of Wifi, no notification is shown
         // TODO generalize for automated uploads
-
     }
 
     /**
@@ -1058,7 +1135,8 @@ public class FileUploader extends Service
         if (!uploadResult.isCancelled() &&
             !ResultCode.LOCAL_FILE_NOT_FOUND.equals(uploadResult.getCode()) &&
             !uploadResult.getCode().equals(ResultCode.DELAYED_FOR_WIFI) &&
-            !uploadResult.getCode().equals(ResultCode.DELAYED_FOR_CHARGING)) {
+            !uploadResult.getCode().equals(ResultCode.DELAYED_FOR_CHARGING) &&
+            !uploadResult.getCode().equals(ResultCode.LOCK_FAILED)    ) {
 
             int tickerId = (uploadResult.isSuccess()) ? R.string.uploader_upload_succeeded_ticker :
                     R.string.uploader_upload_failed_ticker;
@@ -1077,9 +1155,7 @@ public class FileUploader extends Service
                     .setOngoing(false)
                     .setProgress(0, 0, false);
 
-            content = ErrorMessageAdapter.getErrorCauseMessage(
-                    uploadResult, upload, getResources()
-            );
+            content = ErrorMessageAdapter.getErrorCauseMessage(uploadResult, upload, getResources());
 
             if (needsToUpdateCredentials) {
                 // let the user update credentials with one click
@@ -1130,7 +1206,6 @@ public class FileUploader extends Service
         }
     }
 
-
     /**
      * Sends a broadcast in order to the interested activities can update their
      * view
@@ -1144,7 +1219,6 @@ public class FileUploader extends Service
         sendStickyBroadcast(start);
     }
 
-
     /**
      * Sends a broadcast in order to the interested activities can update their
      * view
@@ -1208,5 +1282,4 @@ public class FileUploader extends Service
         mPendingUploads.remove(account.name);
         mUploadsStorageManager.removeUploads(account.name);
     }
-
 }

+ 2 - 2
src/main/java/com/owncloud/android/services/AccountRemovalJob.java → src/main/java/com/owncloud/android/jobs/AccountRemovalJob.java

@@ -19,7 +19,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package com.owncloud.android.services;
+package com.owncloud.android.jobs;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
@@ -76,7 +76,7 @@ public class AccountRemovalJob extends Job implements AccountManagerCallback<Boo
 
             // remove pending account removal
             ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(context.getContentResolver());
-            arbitraryDataProvider.deleteKeyForAccount(account, PENDING_FOR_REMOVAL);
+            arbitraryDataProvider.deleteKeyForAccount(account.name, PENDING_FOR_REMOVAL);
 
             return Result.SUCCESS;
         } else {

+ 6 - 3
src/main/java/com/owncloud/android/services/ContactsBackupJob.java → src/main/java/com/owncloud/android/jobs/ContactsBackupJob.java

@@ -19,7 +19,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package com.owncloud.android.services;
+package com.owncloud.android.jobs;
 
 import android.accounts.Account;
 import android.content.ComponentName;
@@ -44,6 +44,7 @@ import com.owncloud.android.datamodel.OCFile;
 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.services.OperationsService;
 import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
 
 import java.io.File;
@@ -97,7 +98,7 @@ public class ContactsBackupJob extends Job {
                     OperationsService.BIND_AUTO_CREATE);
 
             // store execution date
-            arbitraryDataProvider.storeOrUpdateKeyValue(account,
+            arbitraryDataProvider.storeOrUpdateKeyValue(account.name,
                     ContactsPreferenceActivity.PREFERENCE_CONTACTS_LAST_BACKUP,
                     String.valueOf(Calendar.getInstance().getTimeInMillis()));
         } else {
@@ -156,7 +157,9 @@ public class ContactsBackupJob extends Job {
                     FileUploader.LOCAL_BEHAVIOUR_MOVE,
                     null,
                     true,
-                    UploadFileOperation.CREATED_BY_USER
+                    UploadFileOperation.CREATED_BY_USER,
+                    false,
+                    false
             );
 
         } catch (Exception e) {

+ 1 - 1
src/main/java/com/owncloud/android/services/ContactsImportJob.java → src/main/java/com/owncloud/android/jobs/ContactsImportJob.java

@@ -19,7 +19,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package com.owncloud.android.services;
+package com.owncloud.android.jobs;
 
 import android.database.Cursor;
 import android.net.Uri;

+ 153 - 0
src/main/java/com/owncloud/android/jobs/FilesSyncJob.java

@@ -0,0 +1,153 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2017 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.jobs;
+
+import android.accounts.Account;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.PowerManager;
+import android.support.annotation.NonNull;
+import android.support.media.ExifInterface;
+import android.text.TextUtils;
+
+import com.evernote.android.job.Job;
+import com.evernote.android.job.util.support.PersistableBundleCompat;
+import com.owncloud.android.MainApp;
+import com.owncloud.android.authentication.AccountUtils;
+import com.owncloud.android.datamodel.FilesystemDataProvider;
+import com.owncloud.android.datamodel.MediaFolderType;
+import com.owncloud.android.datamodel.SyncedFolder;
+import com.owncloud.android.datamodel.SyncedFolderProvider;
+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.FilesSyncHelper;
+import com.owncloud.android.utils.MimeTypeUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/*
+    Job that:
+        - restarts existing jobs if required
+        - finds new and modified files since we last run this
+        - creates upload tasks
+ */
+public class FilesSyncJob extends Job {
+    public static final String TAG = "FilesSyncJob";
+
+    public static final String SKIP_CUSTOM = "skipCustom";
+
+    @NonNull
+    @Override
+    protected Result onRunJob(Params params) {
+        final Context context = MainApp.getAppContext();
+        final ContentResolver contentResolver = context.getContentResolver();
+
+        PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
+        PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+                TAG);
+        wakeLock.acquire();
+
+        PersistableBundleCompat bundle = params.getExtras();
+        final boolean skipCustom = bundle.getBoolean(SKIP_CUSTOM, false);
+
+        FilesSyncHelper.restartJobsIfNeeded();
+        FilesSyncHelper.insertAllDBEntries(skipCustom);
+
+        // Create all the providers we'll need
+        final FilesystemDataProvider filesystemDataProvider = new FilesystemDataProvider(contentResolver);
+        SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(contentResolver);
+
+        for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
+            if ((syncedFolder.isEnabled()) && (!skipCustom || MediaFolderType.CUSTOM != syncedFolder.getType())) {
+                for (String path : filesystemDataProvider.getFilesForUpload(syncedFolder.getLocalPath(),
+                        Long.toString(syncedFolder.getId()))) {
+                    File file = new File(path);
+
+                    Long lastModificationTime = file.lastModified();
+                    final Locale currentLocale = context.getResources().getConfiguration().locale;
+
+                    if (MediaFolderType.IMAGE == syncedFolder.getType()) {
+                        String mimetypeString = FileStorageUtils.getMimeTypeFromName(file.getAbsolutePath());
+                        if ("image/jpeg".equalsIgnoreCase(mimetypeString) || "image/tiff".
+                                equalsIgnoreCase(mimetypeString)) {
+                            try {
+                                ExifInterface exifInterface = new ExifInterface(file.getAbsolutePath());
+                                String exifDate = exifInterface.getAttribute(ExifInterface.TAG_DATETIME);
+                                if (!TextUtils.isEmpty(exifDate)) {
+                                    ParsePosition pos = new ParsePosition(0);
+                                    SimpleDateFormat sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss",
+                                            currentLocale);
+                                    sFormatter.setTimeZone(TimeZone.getTimeZone(TimeZone.getDefault().getID()));
+                                    Date dateTime = sFormatter.parse(exifDate, pos);
+                                    lastModificationTime = dateTime.getTime();
+                                }
+
+                            } catch (IOException e) {
+                                Log_OC.d(TAG, "Failed to get the proper time " + e.getLocalizedMessage());
+                            }
+                        }
+                    }
+
+                    boolean needsCharging = syncedFolder.getChargingOnly();
+                    boolean needsWifi = syncedFolder.getWifiOnly();
+
+                    String mimeType = MimeTypeUtil.getBestMimeTypeByFilename(file.getAbsolutePath());
+
+                    Account account = AccountUtils.getOwnCloudAccountByName(context, syncedFolder.getAccount());
+
+                    requester.uploadFileWithOverwrite(
+                            context,
+                            account,
+                            file.getAbsolutePath(),
+                            FileStorageUtils.getInstantUploadFilePath(
+                                    currentLocale,
+                                    syncedFolder.getRemotePath(), file.getName(),
+                                    lastModificationTime,
+                                    syncedFolder.getSubfolderByDate()),
+                            syncedFolder.getUploadAction(),
+                            mimeType,
+                            true,           // create parent folder if not existent
+                            UploadFileOperation.CREATED_AS_INSTANT_PICTURE,
+                            needsWifi,
+                            needsCharging,
+                            true
+                    );
+
+                    filesystemDataProvider.updateFilesystemFileAsSentForUpload(path,
+                            Long.toString(syncedFolder.getId()));
+                }
+            }
+        }
+
+        wakeLock.release();
+        return Result.SUCCESS;
+    }
+}

+ 3 - 3
src/main/java/com/owncloud/android/services/NCJobCreator.java → src/main/java/com/owncloud/android/jobs/NCJobCreator.java

@@ -18,7 +18,7 @@
  * 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;
+package com.owncloud.android.jobs;
 
 import com.evernote.android.job.Job;
 import com.evernote.android.job.JobCreator;
@@ -31,14 +31,14 @@ public class NCJobCreator implements JobCreator {
     @Override
     public Job create(String tag) {
         switch (tag) {
-            case AutoUploadJob.TAG:
-                return new AutoUploadJob();
             case ContactsBackupJob.TAG:
                 return new ContactsBackupJob();
             case ContactsImportJob.TAG:
                 return new ContactsImportJob();
             case AccountRemovalJob.TAG:
                 return new AccountRemovalJob();
+            case FilesSyncJob.TAG:
+                return new FilesSyncJob();
             default:
                 return null;
         }

+ 69 - 0
src/main/java/com/owncloud/android/jobs/NContentObserverJob.java

@@ -0,0 +1,69 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2017 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.jobs;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+
+import com.evernote.android.job.JobRequest;
+import com.evernote.android.job.util.support.PersistableBundleCompat;
+import com.owncloud.android.utils.FilesSyncHelper;
+
+import java.util.concurrent.TimeUnit;
+
+/*
+    Job that triggers new FilesSyncJob in case new photo or video were detected
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+public class NContentObserverJob extends JobService {
+    @Override
+    public boolean onStartJob(JobParameters params) {
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            if (params.getJobId() == FilesSyncHelper.ContentSyncJobId && params.getTriggeredContentAuthorities()
+                    != null && params.getTriggeredContentUris() != null
+                    && params.getTriggeredContentUris().length > 0) {
+
+                PersistableBundleCompat persistableBundleCompat = new PersistableBundleCompat();
+                persistableBundleCompat.putBoolean(FilesSyncJob.SKIP_CUSTOM, true);
+
+                new JobRequest.Builder(FilesSyncJob.TAG)
+                        .setExecutionWindow(1, TimeUnit.SECONDS.toMillis(2))
+                        .setBackoffCriteria(TimeUnit.SECONDS.toMillis(5), JobRequest.BackoffPolicy.LINEAR)
+                        .setUpdateCurrent(false)
+                        .build()
+                        .schedule();
+            }
+
+            FilesSyncHelper.scheduleNJobs(true);
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        return false;
+    }
+}

+ 2 - 7
src/main/java/com/owncloud/android/operations/MoveFileOperation.java

@@ -20,8 +20,6 @@
 
 package com.owncloud.android.operations;
 
-import android.accounts.Account;
-
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
@@ -41,17 +39,14 @@ public class MoveFileOperation extends SyncOperation {
     private String mTargetParentPath;
     
     private OCFile mFile;
-
-    
     
     /**
      * Constructor
      * 
      * @param srcPath           Remote path of the {@link OCFile} to move.
      * @param targetParentPath  Path to the folder where the file will be moved into.
-     * @param account           OwnCloud account containing both the file and the target folder 
      */
-    public MoveFileOperation(String srcPath, String targetParentPath, Account account) {
+    public MoveFileOperation(String srcPath, String targetParentPath) {
         mSrcPath = srcPath;
         mTargetParentPath = targetParentPath;
         if (!mTargetParentPath.endsWith(OCFile.PATH_SEPARATOR)) {
@@ -68,7 +63,7 @@ public class MoveFileOperation extends SyncOperation {
      */
     @Override
     protected RemoteOperationResult run(OwnCloudClient client) {
-        RemoteOperationResult result = null;
+        RemoteOperationResult result;
         
         /// 1. check move validity
         if (mTargetParentPath.startsWith(mSrcPath)) {

+ 4 - 7
src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java

@@ -203,7 +203,7 @@ public class RefreshFolderOperation extends RemoteOperation {
 
             if (result.isSuccess()) {
                 // request for the synchronization of KEPT-IN-SYNC file contents
-                startContentSynchronizations(mFilesToSyncContents, client);
+                startContentSynchronizations(mFilesToSyncContents);
             }
         }
         
@@ -453,12 +453,9 @@ public class RefreshFolderOperation extends RemoteOperation {
      * on.
      * 
      * @param filesToSyncContents       Synchronization operations to execute.
-     * @param client                    Interface to the remote ownCloud server.
      */
-    private void startContentSynchronizations(
-            List<SynchronizeFileOperation> filesToSyncContents, OwnCloudClient client
-        ) {
-        RemoteOperationResult contentsResult = null;
+    private void startContentSynchronizations(List<SynchronizeFileOperation> filesToSyncContents) {
+        RemoteOperationResult contentsResult;
         for (SynchronizeFileOperation op: filesToSyncContents) {
             contentsResult = op.execute(mStorageManager, mContext);   // async
             if (!contentsResult.isSuccess()) {
@@ -487,7 +484,7 @@ public class RefreshFolderOperation extends RemoteOperation {
      *                  the operation.
      */
     private RemoteOperationResult refreshSharesForFolder(OwnCloudClient client) {
-        RemoteOperationResult result = null;
+        RemoteOperationResult result;
         
         // remote request 
         GetRemoteSharesForFileOperation operation = 

+ 0 - 13
src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java

@@ -469,19 +469,6 @@ public class SynchronizeFolderOperation extends SyncOperation {
         }
     }
 
-    
-    /**
-     * Creates and populates a new {@link com.owncloud.android.datamodel.OCFile}
-     * object with the data read from the server.
-     *
-     * @param remote    remote file read from the server (remote file or folder).
-     * @return          New OCFile instance representing the remote resource described by we.
-     */
-    private OCFile fillOCFile(RemoteFile remote) {
-        return FileStorageUtils.fillOCFile(remote);
-    }
-
-
     /**
      * Scans the default location for saving local copies of files searching for
      * a 'lost' file with the same full name as the {@link com.owncloud.android.datamodel.OCFile}

+ 126 - 86
src/main/java/com/owncloud/android/operations/UploadFileOperation.java

@@ -1,21 +1,20 @@
 /**
- *   ownCloud Android client application
+ * ownCloud Android client application
  *
- *   @author David A. Velasco
- *   Copyright (C) 2016 ownCloud GmbH.
+ * @author David A. Velasco
+ * Copyright (C) 2016 ownCloud GmbH.
  *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License version 2,
- *   as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
  *
- *   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 General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 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 General Public License for more details.
  *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
 package com.owncloud.android.operations;
@@ -24,11 +23,12 @@ import android.accounts.Account;
 import android.content.Context;
 import android.net.Uri;
 
+import com.evernote.android.job.JobRequest;
+import com.evernote.android.job.util.Device;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.db.OCUpload;
-import com.owncloud.android.db.PreferenceManager;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
@@ -44,7 +44,6 @@ import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation;
 import com.owncloud.android.lib.resources.files.RemoteFile;
 import com.owncloud.android.lib.resources.files.UploadRemoteFileOperation;
 import com.owncloud.android.operations.common.SyncOperation;
-import com.owncloud.android.utils.ConnectivityUtils;
 import com.owncloud.android.utils.FileStorageUtils;
 import com.owncloud.android.utils.MimeType;
 import com.owncloud.android.utils.MimeTypeUtil;
@@ -52,14 +51,20 @@ import com.owncloud.android.utils.UriUtils;
 
 import org.apache.commons.httpclient.HttpStatus;
 import org.apache.commons.httpclient.methods.RequestEntity;
+import org.lukhnos.nnio.file.Files;
+import org.lukhnos.nnio.file.Paths;
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.RandomAccessFile;
 import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Set;
@@ -94,6 +99,8 @@ public class UploadFileOperation extends SyncOperation {
     private boolean mForceOverwrite = false;
     private int mLocalBehaviour = FileUploader.LOCAL_BEHAVIOUR_COPY;
     private int mCreatedBy = CREATED_BY_USER;
+    private boolean mOnWifiOnly = false;
+    private boolean mWhileChargingOnly = false;
 
     private boolean mWasRenamed = false;
     private long mOCUploadId = -1;
@@ -147,7 +154,9 @@ public class UploadFileOperation extends SyncOperation {
                                boolean chunked,
                                boolean forceOverwrite,
                                int localBehaviour,
-                               Context context
+                               Context context,
+                               boolean onWifiOnly,
+                               boolean whileChargingOnly
     ) {
         if (account == null) {
             throw new IllegalArgumentException("Illegal NULL account in UploadFileOperation " + "creation");
@@ -171,6 +180,8 @@ public class UploadFileOperation extends SyncOperation {
         } else {
             mFile = file;
         }
+        mOnWifiOnly = onWifiOnly;
+        mWhileChargingOnly = whileChargingOnly;
         mRemotePath = upload.getRemotePath();
         mChunked = chunked;
         mForceOverwrite = forceOverwrite;
@@ -182,6 +193,14 @@ public class UploadFileOperation extends SyncOperation {
         mRemoteFolderToBeCreated = upload.isCreateRemoteFolder();
     }
 
+    public boolean getIsWifiRequired() {
+        return mOnWifiOnly;
+    }
+
+    public boolean getIsChargingRequired() {
+        return mWhileChargingOnly;
+    }
+
     public Account getAccount() {
         return mAccount;
     }
@@ -237,7 +256,7 @@ public class UploadFileOperation extends SyncOperation {
         }
     }
 
-    public int getCreatedBy () {
+    public int getCreatedBy() {
         return mCreatedBy;
     }
 
@@ -249,9 +268,10 @@ public class UploadFileOperation extends SyncOperation {
         return mCreatedBy == CREATED_AS_INSTANT_VIDEO;
     }
 
-    public void setOCUploadId(long id){
+    public void setOCUploadId(long id) {
         mOCUploadId = id;
     }
+
     public long getOCUploadId() {
         return mOCUploadId;
     }
@@ -260,14 +280,14 @@ public class UploadFileOperation extends SyncOperation {
         return mDataTransferListeners;
     }
 
-    public void addDatatransferProgressListener (OnDatatransferProgressListener listener) {
+    public void addDatatransferProgressListener(OnDatatransferProgressListener listener) {
         synchronized (mDataTransferListeners) {
             mDataTransferListeners.add(listener);
         }
         if (mEntity != null) {
-            ((ProgressiveDataTransferer)mEntity).addDatatransferProgressListener(listener);
+            ((ProgressiveDataTransferer) mEntity).addDatatransferProgressListener(listener);
         }
-        if(mUploadOperation != null){
+        if (mUploadOperation != null) {
             mUploadOperation.addDatatransferProgressListener(listener);
         }
     }
@@ -277,14 +297,14 @@ public class UploadFileOperation extends SyncOperation {
             mDataTransferListeners.remove(listener);
         }
         if (mEntity != null) {
-            ((ProgressiveDataTransferer)mEntity).removeDatatransferProgressListener(listener);
+            ((ProgressiveDataTransferer) mEntity).removeDatatransferProgressListener(listener);
         }
-        if(mUploadOperation != null){
+        if (mUploadOperation != null) {
             mUploadOperation.removeDatatransferProgressListener(listener);
         }
     }
 
-    public void addRenameUploadListener (OnRenameListener listener) {
+    public void addRenameUploadListener(OnRenameListener listener) {
         mRenameUploadListener = listener;
     }
 
@@ -297,17 +317,18 @@ public class UploadFileOperation extends SyncOperation {
         File temporalFile = null;
         File originalFile = new File(mOriginalStoragePath);
         File expectedFile = null;
+        FileLock fileLock = null;
 
         try {
 
             /// Check that connectivity conditions are met and delays the upload otherwise
-            if (delayForWifi()) {
+            if (mOnWifiOnly && !Device.getNetworkType(mContext).equals(JobRequest.NetworkType.UNMETERED)) {
                 Log_OC.d(TAG, "Upload delayed until WiFi is available: " + getRemotePath());
                 return new RemoteOperationResult(ResultCode.DELAYED_FOR_WIFI);
             }
 
             // Check if charging conditions are met and delays the upload otherwise
-            if (delayForCharging()){
+            if (mWhileChargingOnly && !Device.isCharging(mContext)) {
                 Log_OC.d(TAG, "Upload delayed until the device is charging: " + getRemotePath());
                 return new RemoteOperationResult(ResultCode.DELAYED_FOR_CHARGING);
             }
@@ -372,20 +393,21 @@ public class UploadFileOperation extends SyncOperation {
             }
 
             // Get the last modification date of the file from the file system
-            Long timeStampLong = originalFile.lastModified()/1000;
+            Long timeStampLong = originalFile.lastModified() / 1000;
             String timeStamp = timeStampLong.toString();
 
             /// perform the upload
-            if ( mChunked &&
+            if (mChunked &&
                     (new File(mFile.getStoragePath())).length() >
-                            ChunkedUploadRemoteFileOperation.CHUNK_SIZE ) {
+                            ChunkedUploadRemoteFileOperation.CHUNK_SIZE) {
                 mUploadOperation = new ChunkedUploadRemoteFileOperation(mContext, mFile.getStoragePath(),
                         mFile.getRemotePath(), mFile.getMimetype(), mFile.getEtagInConflict(), timeStamp);
             } else {
                 mUploadOperation = new UploadRemoteFileOperation(mFile.getStoragePath(),
                         mFile.getRemotePath(), mFile.getMimetype(), mFile.getEtagInConflict(), timeStamp);
             }
-            Iterator <OnDatatransferProgressListener> listener = mDataTransferListeners.iterator();
+
+            Iterator<OnDatatransferProgressListener> listener = mDataTransferListeners.iterator();
             while (listener.hasNext()) {
                 mUploadOperation.addDatatransferProgressListener(listener.next());
             }
@@ -394,23 +416,77 @@ public class UploadFileOperation extends SyncOperation {
                 throw new OperationCancelledException();
             }
 
+            FileChannel channel = null;
+            try {
+                channel = new RandomAccessFile(mFile.getStoragePath(), "rw").getChannel();
+                fileLock = channel.tryLock();
+            } catch (FileNotFoundException e) {
+                if (temporalFile == null) {
+                    String temporalPath = FileStorageUtils.getTemporalPath(mAccount.name) + mFile.getRemotePath();
+                    mFile.setStoragePath(temporalPath);
+                    temporalFile = new File(temporalPath);
+
+                    result = copy(originalFile, temporalFile);
+
+                    if (result != null) {
+                        return result;
+                    } else {
+                        if (temporalFile.length() == originalFile.length()) {
+                            channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").getChannel();
+                            fileLock = channel.tryLock();
+                        } else {
+                            while (temporalFile.length() != originalFile.length()) {
+                                Files.deleteIfExists(Paths.get(temporalPath));
+                                result = copy(originalFile, temporalFile);
+
+                                if (result != null) {
+                                    return result;
+                                } else {
+                                    channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").
+                                            getChannel();
+                                    fileLock = channel.tryLock();
+                                }
+                            }
+                        }
+                    }
+                } else {
+                    channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").getChannel();
+                    fileLock = channel.tryLock();
+                }
+            }
+
             result = mUploadOperation.execute(client);
 
             /// move local temporal file or original file to its corresponding
             // location in the ownCloud local folder
-            if (!result.isSuccess() && result.getHttpCode() == HttpStatus.SC_PRECONDITION_FAILED ) {
+            if (!result.isSuccess() && result.getHttpCode() == HttpStatus.SC_PRECONDITION_FAILED) {
                 result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);
             }
 
+        } catch (FileNotFoundException e) {
+            Log_OC.d(TAG, mOriginalStoragePath + " not exists anymore");
+            result = new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND);
+        } catch (OverlappingFileLockException e) {
+            Log_OC.d(TAG, "Overlapping file lock exception");
+            result = new RemoteOperationResult(ResultCode.LOCK_FAILED);
         } catch (Exception e) {
             result = new RemoteOperationResult(e);
 
         } finally {
             mUploadStarted.set(false);
+
+            if (fileLock != null) {
+                try {
+                    fileLock.release();
+                } catch (IOException e) {
+                    Log_OC.e(TAG, "Failed to unlock file with path " + mOriginalStoragePath);
+                }
+            }
+
             if (temporalFile != null && !originalFile.equals(temporalFile)) {
                 temporalFile.delete();
             }
-            if (result == null){
+            if (result == null) {
                 result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR);
             }
             if (result.isSuccess()) {
@@ -418,7 +494,7 @@ public class UploadFileOperation extends SyncOperation {
                         result.getLogMessage());
             } else {
                 if (result.getException() != null) {
-                    if(result.isCancelled()){
+                    if (result.isCancelled()) {
                         Log_OC.w(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath +
                                 ": " + result.getLogMessage());
                     } else {
@@ -478,41 +554,6 @@ public class UploadFileOperation extends SyncOperation {
         return result;
     }
 
-
-    /**
-     * Checks origin of current upload and network type to decide if should be delayed, according to
-     * current user preferences.
-     *
-     * @return      'True' if the upload was delayed until WiFi connectivity is available, 'false' otherwise.
-     */
-    private boolean delayForWifi() {
-        boolean delayInstantPicture = (
-            isInstantPicture() &&  PreferenceManager.instantPictureUploadViaWiFiOnly(mContext)
-        );
-        boolean delayInstantVideo = (
-            isInstantVideo() && PreferenceManager.instantVideoUploadViaWiFiOnly(mContext)
-        );
-        return (
-            (delayInstantPicture || delayInstantVideo) &&
-            !ConnectivityUtils.isAppConnectedViaUnmeteredWiFi(mContext)
-        );
-    }
-
-    /**
-     * Check if upload should be delayed due to not charging
-     *
-     * @return      'True' if the upload was delayed until device is charging, 'false' otherwise.
-     */
-    private boolean delayForCharging() {
-        boolean delayInstantPicture = isInstantPicture() &&
-                PreferenceManager.instantPictureUploadWhenChargingOnly(mContext);
-
-        boolean delayInstantVideo = isInstantVideo() && PreferenceManager.instantVideoUploadWhenChargingOnly(mContext);
-
-        return ((delayInstantPicture || delayInstantVideo) && !ConnectivityUtils.isCharging(mContext));
-    }
-
-
     /**
      * Checks the existence of the folder where the current file will be uploaded both
      * in the remote server and in the local database.
@@ -566,7 +607,8 @@ public class UploadFileOperation extends SyncOperation {
 
     /**
      * Create a new OCFile mFile with new remote path. This is required if forceOverwrite==false.
-     * New file is stored as mFile, original as mOldFile. 
+     * New file is stored as mFile, original as mOldFile.
+     *
      * @param newRemotePath new remote path
      */
     private void createNewOCFile(String newRemotePath) {
@@ -615,8 +657,7 @@ public class UploadFileOperation extends SyncOperation {
             suffix = " (" + count + ")";
             if (pos >= 0) {
                 check = existsFile(wc, remotePath + suffix + "." + extension);
-            }
-            else {
+            } else {
                 check = existsFile(wc, remotePath + suffix);
             }
             count++;
@@ -629,7 +670,7 @@ public class UploadFileOperation extends SyncOperation {
         }
     }
 
-    private boolean existsFile(OwnCloudClient client, String remotePath){
+    private boolean existsFile(OwnCloudClient client, String remotePath) {
         ExistenceCheckRemoteOperation existsOperation =
                 new ExistenceCheckRemoteOperation(remotePath, mContext, false);
         RemoteOperationResult result = existsOperation.execute(client);
@@ -667,10 +708,10 @@ public class UploadFileOperation extends SyncOperation {
      * TODO rewrite with homogeneous fail handling, remove dependency on {@link RemoteOperationResult},
      * TODO     use Exceptions instead
      *
-     * @param   sourceFile      Source file to copy.
-     * @param   targetFile      Target location to copy the file.
-     * @return  {@link RemoteOperationResult}
-     * @throws  IOException
+     * @param sourceFile Source file to copy.
+     * @param targetFile Target location to copy the file.
+     * @return {@link RemoteOperationResult}
+     * @throws IOException
      */
     private RemoteOperationResult copy(File sourceFile, File targetFile) throws IOException {
         Log_OC.d(TAG, "Copying local file");
@@ -758,10 +799,10 @@ public class UploadFileOperation extends SyncOperation {
      *
      * TODO refactor both this and 'copy' in a single method
      *
-     * @param   sourceFile      Source file to move.
-     * @param   targetFile      Target location to move the file.
-     * @return  {@link RemoteOperationResult}
-     * @throws  IOException
+     * @param sourceFile Source file to move.
+     * @param targetFile Target location to move the file.
+     * @return {@link RemoteOperationResult}
+     * @throws IOException
      */
     private void move(File sourceFile, File targetFile) throws IOException {
 
@@ -769,8 +810,8 @@ public class UploadFileOperation extends SyncOperation {
             File expectedFolder = targetFile.getParentFile();
             expectedFolder.mkdirs();
 
-            if (expectedFolder.isDirectory()){
-                if (!sourceFile.renameTo(targetFile)){
+            if (expectedFolder.isDirectory()) {
+                if (!sourceFile.renameTo(targetFile)) {
                     // try to copy and then delete
                     targetFile.createNewFile();
                     FileChannel inChannel = new FileInputStream(sourceFile).getChannel();
@@ -778,12 +819,11 @@ public class UploadFileOperation extends SyncOperation {
                     try {
                         inChannel.transferTo(0, inChannel.size(), outChannel);
                         sourceFile.delete();
-                    } catch (Exception e){
+                    } catch (Exception e) {
                         mFile.setStoragePath(""); // forget the local file
                         // by now, treat this as a success; the file was uploaded
                         // the best option could be show a warning message
-                    }
-                    finally {
+                    } finally {
                         if (inChannel != null) {
                             inChannel.close();
                         }
@@ -847,7 +887,7 @@ public class UploadFileOperation extends SyncOperation {
 
         // generate new Thumbnail
         final ThumbnailsCacheManager.ThumbnailGenerationTask task =
-            new ThumbnailsCacheManager.ThumbnailGenerationTask(getStorageManager(), mAccount);
+                new ThumbnailsCacheManager.ThumbnailGenerationTask(getStorageManager(), mAccount);
         task.execute(file);
     }
 

+ 1 - 1
src/main/java/org/nextcloud/providers/DocumentsStorageProvider.java → src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java

@@ -18,7 +18,7 @@
  *
  */
 
-package org.nextcloud.providers;
+package com.owncloud.android.providers;
 
 import android.accounts.Account;
 import android.annotation.TargetApi;

+ 92 - 11
src/main/java/com/owncloud/android/providers/FileContentProvider.java

@@ -74,6 +74,7 @@ public class FileContentProvider extends ContentProvider {
     private static final int EXTERNAL_LINKS = 8;
     private static final int ARBITRARY_DATA = 9;
     private static final int VIRTUAL = 10;
+    private static final int FILESYSTEM = 11;
 
     private static final String TAG = FileContentProvider.class.getSimpleName();
 
@@ -209,6 +210,9 @@ public class FileContentProvider extends ContentProvider {
             case VIRTUAL:
                 count = db.delete(ProviderTableMeta.VIRTUAL_TABLE_NAME, where, whereArgs);
                 break;
+            case FILESYSTEM:
+                count = db.delete(ProviderTableMeta.FILESYSTEM_TABLE_NAME, where, whereArgs);
+                break;
             default:
                 //Log_OC.e(TAG, "Unknown uri " + uri);
                 throw new IllegalArgumentException("Unknown uri: " + uri.toString());
@@ -312,7 +316,6 @@ public class FileContentProvider extends ContentProvider {
                 if (uploadId > 0) {
                     insertedUploadUri =
                             ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_UPLOADS, uploadId);
-                    trimSuccessfulUploads(db);
                 } else {
                     throw new SQLException(ERROR + uri);
 
@@ -365,6 +368,16 @@ public class FileContentProvider extends ContentProvider {
                 }
 
                 return insertedVirtualUri;
+            case FILESYSTEM:
+                Uri insertedFilesystemUri = null;
+                long filesystedId = db.insert(ProviderTableMeta.FILESYSTEM_TABLE_NAME, null, values);
+                if (filesystedId > 0) {
+                    insertedFilesystemUri =
+                            ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILESYSTEM, filesystedId);
+                } else {
+                    throw new SQLException("ERROR " + uri);
+                }
+                return insertedFilesystemUri;
             default:
                 throw new IllegalArgumentException("Unknown uri id: " + uri);
         }
@@ -417,6 +430,7 @@ public class FileContentProvider extends ContentProvider {
         mUriMatcher.addURI(authority, "external_links", EXTERNAL_LINKS);
         mUriMatcher.addURI(authority, "arbitrary_data", ARBITRARY_DATA);
         mUriMatcher.addURI(authority, "virtual", VIRTUAL);
+        mUriMatcher.addURI(authority, "filesystem", FILESYSTEM);
 
         return true;
     }
@@ -518,6 +532,13 @@ public class FileContentProvider extends ContentProvider {
                     sqlQuery.appendWhere(ProviderTableMeta._ID + "=" + uri.getPathSegments().get(1));
                 }
                 break;
+            case FILESYSTEM:
+                sqlQuery.setTables(ProviderTableMeta.FILESYSTEM_TABLE_NAME);
+                if (uri.getPathSegments().size() > 1) {
+                    sqlQuery.appendWhere(ProviderTableMeta._ID + "="
+                            + uri.getPathSegments().get(1));
+                }
+                break;
             default:
                 throw new IllegalArgumentException("Unknown uri id: " + uri);
         }
@@ -549,6 +570,9 @@ public class FileContentProvider extends ContentProvider {
                 default: // Files
                     order = ProviderTableMeta.FILE_DEFAULT_SORT_ORDER;
                     break;
+                case FILESYSTEM:
+                    order = ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH;
+                    break;
             }
         } else {
             order = sortOrder;
@@ -594,12 +618,13 @@ public class FileContentProvider extends ContentProvider {
                 return db.update(ProviderTableMeta.CAPABILITIES_TABLE_NAME, values, selection, selectionArgs);
             case UPLOADS:
                 int ret = db.update(ProviderTableMeta.UPLOADS_TABLE_NAME, values, selection, selectionArgs);
-                trimSuccessfulUploads(db);
                 return ret;
             case SYNCED_FOLDERS:
                 return db.update(ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME, values, selection, selectionArgs);
             case ARBITRARY_DATA:
                 return db.update(ProviderTableMeta.ARBITRARY_DATA_TABLE_NAME, values, selection, selectionArgs);
+            case FILESYSTEM:
+                return db.update(ProviderTableMeta.FILESYSTEM_TABLE_NAME, values, selection, selectionArgs);
             default:
                 return db.update(ProviderTableMeta.FILE_TABLE_NAME, values, selection, selectionArgs);
         }
@@ -662,6 +687,9 @@ public class FileContentProvider extends ContentProvider {
 
             // Create virtual table
             createVirtualTable(db);
+
+            // Create filesystem table
+            createFileSystemTable(db);
         }
 
         @Override
@@ -669,14 +697,14 @@ public class FileContentProvider extends ContentProvider {
             Log_OC.i(SQL, "Entering in onUpgrade");
             boolean upgraded = false;
             if (oldVersion == 1 && newVersion >= 2) {
-                Log_OC.i(SQL, "Entering in the #1 ADD in onUpgrade");
+                Log_OC.i(SQL, "Entering in the #2 ADD in onUpgrade");
                 db.execSQL(ALTER_TABLE + ProviderTableMeta.FILE_TABLE_NAME +
                         ADD_COLUMN + ProviderTableMeta.FILE_KEEP_IN_SYNC + " INTEGER " +
                         " DEFAULT 0");
                 upgraded = true;
             }
             if (oldVersion < 3 && newVersion >= 3) {
-                Log_OC.i(SQL, "Entering in the #2 ADD in onUpgrade");
+                Log_OC.i(SQL, "Entering in the #3 ADD in onUpgrade");
                 db.beginTransaction();
                 try {
                     db.execSQL(ALTER_TABLE + ProviderTableMeta.FILE_TABLE_NAME +
@@ -696,7 +724,7 @@ public class FileContentProvider extends ContentProvider {
                 }
             }
             if (oldVersion < 4 && newVersion >= 4) {
-                Log_OC.i(SQL, "Entering in the #3 ADD in onUpgrade");
+                Log_OC.i(SQL, "Entering in the #4 ADD in onUpgrade");
                 db.beginTransaction();
                 try {
                     db.execSQL(ALTER_TABLE + ProviderTableMeta.FILE_TABLE_NAME +
@@ -720,7 +748,7 @@ public class FileContentProvider extends ContentProvider {
             }
 
             if (oldVersion < 5 && newVersion >= 5) {
-                Log_OC.i(SQL, "Entering in the #4 ADD in onUpgrade");
+                Log_OC.i(SQL, "Entering in the #5 ADD in onUpgrade");
                 db.beginTransaction();
                 try {
                     db.execSQL(ALTER_TABLE + ProviderTableMeta.FILE_TABLE_NAME +
@@ -738,7 +766,7 @@ public class FileContentProvider extends ContentProvider {
             }
 
             if (oldVersion < 6 && newVersion >= 6) {
-                Log_OC.i(SQL, "Entering in the #5 ADD in onUpgrade");
+                Log_OC.i(SQL, "Entering in the #6 ADD in onUpgrade");
                 db.beginTransaction();
                 try {
                     db.execSQL(ALTER_TABLE + ProviderTableMeta.FILE_TABLE_NAME +
@@ -843,6 +871,7 @@ public class FileContentProvider extends ContentProvider {
                     db.endTransaction();
                 }
             }
+
             if (!upgraded) {
                 Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
             }
@@ -937,7 +966,6 @@ public class FileContentProvider extends ContentProvider {
                 } finally {
                     db.endTransaction();
                 }
-
             }
 
             if (!upgraded) {
@@ -1033,6 +1061,43 @@ public class FileContentProvider extends ContentProvider {
             if (!upgraded) {
                 Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
             }
+
+            if (oldVersion < 23 && newVersion >= 23) {
+                Log_OC.i(SQL, "Entering in the #23 adding type column for synced folders, Create filesystem table");
+                db.beginTransaction();
+                try {
+                    // add type column default being CUSTOM (0)
+                    Log_OC.i(SQL, "Add type column and default value 0 (CUSTOM) to synced_folders table");
+                    db.execSQL(ALTER_TABLE + ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME +
+                            ADD_COLUMN + ProviderTableMeta.SYNCED_FOLDER_TYPE +
+                            " INTEGER " + " DEFAULT 0");
+
+                    Log_OC.i(SQL, "Add charging and wifi columns to uploads");
+                    db.execSQL(ALTER_TABLE + ProviderTableMeta.UPLOADS_TABLE_NAME +
+                            ADD_COLUMN + ProviderTableMeta.UPLOADS_IS_WIFI_ONLY +
+                            " INTEGER " + " DEFAULT 0");
+
+                    db.execSQL(ALTER_TABLE + ProviderTableMeta.UPLOADS_TABLE_NAME +
+                            ADD_COLUMN + ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY +
+                            " INTEGER " + " DEFAULT 0");
+
+                    // create Filesystem table
+                    Log_OC.i(SQL, "Create filesystem table");
+                    createFileSystemTable(db);
+
+                    upgraded = true;
+                    db.setTransactionSuccessful();
+
+                } catch (Throwable t) {
+                    Log_OC.e(TAG, "ERROR!", t);
+                } finally {
+                    db.endTransaction();
+                }
+
+                if (!upgraded) {
+                    Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
+                }
+            }
         }
     }
 
@@ -1135,6 +1200,8 @@ public class FileContentProvider extends ContentProvider {
                 + ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER + INTEGER  // boolean
                 + ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP + INTEGER
                 + ProviderTableMeta.UPLOADS_LAST_RESULT + INTEGER     // Upload LastResult
+                + ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY + INTEGER  // boolean
+                + ProviderTableMeta.UPLOADS_IS_WIFI_ONLY + INTEGER // boolean
                 + ProviderTableMeta.UPLOADS_CREATED_BY + " INTEGER );"    // Upload createdBy
         );
 
@@ -1159,7 +1226,8 @@ public class FileContentProvider extends ContentProvider {
                 + 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
+                + ProviderTableMeta.SYNCED_FOLDER_UPLOAD_ACTION + " INTEGER, "     // upload action
+                + ProviderTableMeta.SYNCED_FOLDER_TYPE + " INTEGER );"               // type
         );
     }
 
@@ -1170,7 +1238,7 @@ public class FileContentProvider extends ContentProvider {
                 + ProviderTableMeta.EXTERNAL_LINKS_LANGUAGE + " TEXT, "     // language
                 + ProviderTableMeta.EXTERNAL_LINKS_TYPE + " INTEGER, "      // type
                 + ProviderTableMeta.EXTERNAL_LINKS_NAME + " TEXT, "         // name
-                + ProviderTableMeta.EXTERNAL_LINKS_URL + " TEXT )"          // url
+                + ProviderTableMeta.EXTERNAL_LINKS_URL + " TEXT );"          // url
         );
     }
 
@@ -1179,7 +1247,7 @@ public class FileContentProvider extends ContentProvider {
                 + ProviderTableMeta._ID + " INTEGER PRIMARY KEY, "      // id
                 + ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID + " TEXT, " // cloud id (account name + FQDN)
                 + ProviderTableMeta.ARBITRARY_DATA_KEY + " TEXT, "      // key
-                + ProviderTableMeta.ARBITRARY_DATA_VALUE + " TEXT) "    // value
+                + ProviderTableMeta.ARBITRARY_DATA_VALUE + " TEXT );"    // value
         );
     }
 
@@ -1191,6 +1259,19 @@ public class FileContentProvider extends ContentProvider {
         );
     }
 
+    private void createFileSystemTable(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE " + ProviderTableMeta.FILESYSTEM_TABLE_NAME + "("
+                + ProviderTableMeta._ID + " INTEGER PRIMARY KEY, "      // id
+                + ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH + " TEXT, "
+                + ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER + " INTEGER, "
+                + ProviderTableMeta.FILESYSTEM_FILE_FOUND_RECENTLY + " LONG, "
+                + ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD + " INTEGER, "
+                + ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + " STRING, "
+                + ProviderTableMeta.FILESYSTEM_FILE_MODIFIED + " LONG );"
+        );
+    }
+
+
     /**
      * 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

+ 0 - 237
src/main/java/com/owncloud/android/services/AdvancedFileAlterationListener.java

@@ -1,237 +0,0 @@
-/**
- * Nextcloud Android client application
- *
- * @author Mario Danic
- * Copyright (C) 2017 Mario Danic
- * <p>
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * at your option) any later version.
- * <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.services;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.media.ExifInterface;
-import android.os.Handler;
-import android.text.TextUtils;
-
-import com.evernote.android.job.JobRequest;
-import com.evernote.android.job.util.support.PersistableBundleCompat;
-import com.owncloud.android.MainApp;
-import com.owncloud.android.R;
-import com.owncloud.android.datamodel.ArbitraryDataProvider;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.datamodel.SyncedFolder;
-import com.owncloud.android.files.services.FileUploader;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.ui.activity.Preferences;
-import com.owncloud.android.utils.FileStorageUtils;
-
-import org.apache.commons.io.monitor.FileAlterationListener;
-import org.apache.commons.io.monitor.FileAlterationObserver;
-
-import java.io.File;
-import java.io.IOException;
-import java.text.ParsePosition;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.TimeZone;
-
-/**
- * Magical file alteration listener
- */
-
-public class AdvancedFileAlterationListener implements FileAlterationListener {
-
-    public static final String TAG = "AdvancedFileAlterationListener";
-    public static final int DELAY_INVOCATION_MS = 2500;
-    private Context context;
-    private boolean lightVersion;
-
-    private SyncedFolder syncedFolder;
-
-    private Map<String, Runnable> uploadMap = new HashMap<>();
-    private Handler handler = new Handler();
-
-    public AdvancedFileAlterationListener(SyncedFolder syncedFolder, boolean lightVersion) {
-        super();
-
-        context = MainApp.getAppContext();
-        this.lightVersion = lightVersion;
-        this.syncedFolder = syncedFolder;
-    }
-
-    @Override
-    public void onStart(FileAlterationObserver observer) {
-        // This method is intentionally empty
-    }
-
-    @Override
-    public void onDirectoryCreate(File directory) {
-        // This method is intentionally empty
-    }
-
-    @Override
-    public void onDirectoryChange(File directory) {
-        // This method is intentionally empty
-    }
-
-    @Override
-    public void onDirectoryDelete(File directory) {
-        // This method is intentionally empty
-    }
-
-    @Override
-    public void onFileCreate(final File file) {
-        onFileCreate(file, DELAY_INVOCATION_MS);
-    }
-
-    public void onFileCreate(final File file, int delay) {
-        if (file != null) {
-            uploadMap.put(file.getAbsolutePath(), null);
-
-            String mimetypeString = FileStorageUtils.getMimeTypeFromName(file.getAbsolutePath());
-            Long lastModificationTime = file.lastModified();
-            final Locale currentLocale = context.getResources().getConfiguration().locale;
-
-            if ("image/jpeg".equalsIgnoreCase(mimetypeString) || "image/tiff".equalsIgnoreCase(mimetypeString)) {
-                try {
-                    ExifInterface exifInterface = new ExifInterface(file.getAbsolutePath());
-                    String exifDate = exifInterface.getAttribute(ExifInterface.TAG_DATETIME);
-                    if (!TextUtils.isEmpty(exifDate)) {
-                        ParsePosition pos = new ParsePosition(0);
-                        SimpleDateFormat sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale);
-                        sFormatter.setTimeZone(TimeZone.getTimeZone(TimeZone.getDefault().getID()));
-                        Date dateTime = sFormatter.parse(exifDate, pos);
-                        lastModificationTime = dateTime.getTime();
-                    }
-
-                } catch (IOException e) {
-                    Log_OC.d(TAG, "Failed to get the proper time " + e.getLocalizedMessage());
-                }
-            }
-
-
-            final Long finalLastModificationTime = lastModificationTime;
-
-            Runnable runnable = new Runnable() {
-                @Override
-                public void run() {
-
-                    String remotePath;
-                    boolean subfolderByDate;
-                    boolean chargingOnly;
-                    boolean wifiOnly;
-                    Integer uploadAction;
-                    String accountName = syncedFolder.getAccount();
-
-                    if (lightVersion) {
-                        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(context
-                                .getContentResolver());
-
-                        Resources resources = MainApp.getAppContext().getResources();
-
-                        remotePath = resources.getString(R.string.syncedFolder_remote_folder) + OCFile.PATH_SEPARATOR +
-                                new File(syncedFolder.getLocalPath()).getName() + OCFile.PATH_SEPARATOR;
-                        subfolderByDate = resources.getBoolean(R.bool.syncedFolder_light_use_subfolders);
-                        chargingOnly = resources.getBoolean(R.bool.syncedFolder_light_on_charging);
-                        wifiOnly = arbitraryDataProvider.getBooleanValue(accountName,
-                                Preferences.SYNCED_FOLDER_LIGHT_UPLOAD_ON_WIFI);
-                        String uploadActionString = resources.getString(R.string.syncedFolder_light_upload_behaviour);
-                        uploadAction = getUploadAction(uploadActionString);
-                    } else {
-                        remotePath = syncedFolder.getRemotePath();
-                        subfolderByDate = syncedFolder.getSubfolderByDate();
-                        chargingOnly = syncedFolder.getChargingOnly();
-                        wifiOnly = syncedFolder.getWifiOnly();
-                        uploadAction = syncedFolder.getUploadAction();
-                    }
-
-                    PersistableBundleCompat bundle = new PersistableBundleCompat();
-                    bundle.putString(AutoUploadJob.LOCAL_PATH, file.getAbsolutePath());
-                    bundle.putString(AutoUploadJob.REMOTE_PATH, FileStorageUtils.getInstantUploadFilePath(
-                            currentLocale,
-                            remotePath, file.getName(),
-                            finalLastModificationTime,
-                            subfolderByDate));
-                    bundle.putString(AutoUploadJob.ACCOUNT, accountName);
-                    bundle.putInt(AutoUploadJob.UPLOAD_BEHAVIOUR, uploadAction);
-
-                    new JobRequest.Builder(AutoUploadJob.TAG)
-                            .setExecutionWindow(30_000L, 80_000L)
-                            .setRequiresCharging(chargingOnly)
-                            .setRequiredNetworkType(wifiOnly ? JobRequest.NetworkType.UNMETERED :
-                                    JobRequest.NetworkType.ANY)
-                            .setExtras(bundle)
-                            .setPersisted(false)
-                            .setRequirementsEnforced(true)
-                            .setUpdateCurrent(false)
-                            .build()
-                            .schedule();
-
-                    uploadMap.remove(file.getAbsolutePath());
-                }
-            };
-
-            uploadMap.put(file.getAbsolutePath(), runnable);
-            handler.postDelayed(runnable, delay);
-        }
-    }
-
-    private Integer getUploadAction(String action) {
-        switch (action) {
-            case "LOCAL_BEHAVIOUR_FORGET":
-                return FileUploader.LOCAL_BEHAVIOUR_FORGET;
-            case "LOCAL_BEHAVIOUR_MOVE":
-                return FileUploader.LOCAL_BEHAVIOUR_MOVE;
-            case "LOCAL_BEHAVIOUR_DELETE":
-                return FileUploader.LOCAL_BEHAVIOUR_DELETE;
-            default:
-                return FileUploader.LOCAL_BEHAVIOUR_FORGET;
-        }
-    }
-
-    @Override
-    public void onFileChange(File file) {
-        onFileChange(file, 2500);
-    }
-
-    public void onFileChange(File file, int delay) {
-        Runnable runnable;
-        if ((runnable = uploadMap.get(file.getAbsolutePath())) != null) {
-            handler.removeCallbacks(runnable);
-            handler.postDelayed(runnable, delay);
-        }
-    }
-
-    @Override
-    public void onFileDelete(File file) {
-        Runnable runnable;
-        if ((runnable = uploadMap.get(file.getAbsolutePath())) != null) {
-            handler.removeCallbacks(runnable);
-            uploadMap.remove(file.getAbsolutePath());
-        }
-    }
-
-    @Override
-    public void onStop(FileAlterationObserver observer) {
-        // This method is intentionally empty
-    }
-
-    public int getActiveTasksCount() {
-        return uploadMap.size();
-    }
-}

+ 0 - 81
src/main/java/com/owncloud/android/services/AutoUploadJob.java

@@ -1,81 +0,0 @@
-/**
- * Nextcloud Android client application
- *
- * @author Tobias Kaminsky
- * Copyright (C) 2017 Mario Danic
- * Copyright (C) 2016 Tobias Kaminsky
- * 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.services;
-
-import android.accounts.Account;
-import android.content.Context;
-import android.support.annotation.NonNull;
-
-import com.evernote.android.job.Job;
-import com.evernote.android.job.util.support.PersistableBundleCompat;
-import com.owncloud.android.MainApp;
-import com.owncloud.android.authentication.AccountUtils;
-import com.owncloud.android.files.services.FileUploader;
-import com.owncloud.android.operations.UploadFileOperation;
-import com.owncloud.android.utils.MimeTypeUtil;
-
-import java.io.File;
-
-public class AutoUploadJob extends Job {
-    public static final String TAG = "AutoUploadJob";
-
-    public static final String LOCAL_PATH = "filePath";
-    public static final String REMOTE_PATH = "remotePath";
-    public static final String ACCOUNT = "account";
-    public static final String UPLOAD_BEHAVIOUR = "uploadBehaviour";
-
-    @NonNull
-    @Override
-    protected Result onRunJob(Params params) {
-        final Context context = MainApp.getAppContext();
-        PersistableBundleCompat bundle = params.getExtras();
-        final String filePath = bundle.getString(LOCAL_PATH, "");
-        final String remotePath = bundle.getString(REMOTE_PATH, "");
-        final Account account = AccountUtils.getOwnCloudAccountByName(context, bundle.getString(ACCOUNT, ""));
-        final Integer uploadBehaviour = bundle.getInt(UPLOAD_BEHAVIOUR, FileUploader.LOCAL_BEHAVIOUR_FORGET);
-
-
-        File file = new File(filePath);
-
-
-        // File can be deleted between job generation and job execution. If file does not exist, just ignore it
-        if (file.exists()) {
-            final String mimeType = MimeTypeUtil.getBestMimeTypeByFilename(file.getAbsolutePath());
-
-            final FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-            requester.uploadNewFile(
-                    context,
-                    account,
-                    filePath,
-                    remotePath,
-                    uploadBehaviour,
-                    mimeType,
-                    true,           // create parent folder if not existent
-                    UploadFileOperation.CREATED_AS_INSTANT_PICTURE
-            );
-        }
-
-
-        return Result.SUCCESS;
-    }
-}

+ 1 - 1
src/main/java/com/owncloud/android/services/OperationsService.java

@@ -678,7 +678,7 @@ public class OperationsService extends Service {
                     // Move file/folder
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
                     String newParentPath = operationIntent.getStringExtra(EXTRA_NEW_PARENT_PATH);
-                    operation = new MoveFileOperation(remotePath, newParentPath, account);
+                    operation = new MoveFileOperation(remotePath, newParentPath);
 
                 } else if (action.equals(ACTION_COPY_FILE)) {
                     // Copy file/folder

+ 0 - 402
src/main/java/com/owncloud/android/services/observer/AdvancedFileAlterationObserver.java

@@ -1,402 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.
- *
- * Original source code:
- * https://github.com/apache/commons-io/blob/master/src/main/java/org/apache/commons/io/monitor/FileAlterationObserver.java
- *
- * Modified by Mario Danic
- * Changes are Copyright (C) 2017 Mario Danic
- * Copyright (C) 2017 Nextcloud GmbH
- *
- * All changes are under the same licence as the original.
- *
- */
-package com.owncloud.android.services.observer;
-
-import android.os.SystemClock;
-
-import com.owncloud.android.datamodel.SyncedFolder;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.services.AdvancedFileAlterationListener;
-
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.comparator.NameFileComparator;
-import org.apache.commons.io.monitor.FileAlterationObserver;
-import org.apache.commons.io.monitor.FileEntry;
-
-import java.io.File;
-import java.io.FileFilter;
-import java.io.Serializable;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.Iterator;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-public class AdvancedFileAlterationObserver extends FileAlterationObserver implements Serializable {
-
-    private static final long serialVersionUID = 1185122225658782848L;
-    private static final int DELAY_INVOCATION_MS = 2500;
-    private final List<AdvancedFileAlterationListener> listeners = new CopyOnWriteArrayList<>();
-    private FileEntry rootEntry;
-    private FileFilter fileFilter;
-    private Comparator<File> comparator;
-    private SyncedFolder syncedFolder;
-
-    private static final FileEntry[] EMPTY_ENTRIES = new FileEntry[0];
-    
-    public AdvancedFileAlterationObserver(SyncedFolder syncedFolder, FileFilter fileFilter) {
-        super(syncedFolder.getLocalPath(), fileFilter);
-
-        this.rootEntry = new FileEntry(new File(syncedFolder.getLocalPath()));
-        this.fileFilter = fileFilter;
-        this.syncedFolder = syncedFolder;
-        comparator = NameFileComparator.NAME_SYSTEM_COMPARATOR;
-    }
-
-    public long getSyncedFolderID() {
-        return syncedFolder.getId();
-    }
-
-    public SyncedFolder getSyncedFolder() {
-        return syncedFolder;
-    }
-
-    /**
-     * Return the directory being observed.
-     *
-     * @return the directory being observed
-     */
-    public File getDirectory() {
-        return rootEntry.getFile();
-    }
-
-    /**
-     * Return the fileFilter.
-     *
-     * @return the fileFilter
-     * @since 2.1
-     */
-    public FileFilter getFileFilter() {
-        return fileFilter;
-    }
-
-    public FileEntry getRootEntry() {
-        return rootEntry;
-    }
-
-    public void setRootEntry(FileEntry rootEntry) {
-        this.rootEntry = rootEntry;
-    }
-
-    /**
-     * Add a file system listener.
-     *
-     * @param listener The file system listener
-     */
-    public void addListener(final AdvancedFileAlterationListener listener) {
-        if (listener != null) {
-            listeners.add(listener);
-        }
-    }
-
-    /**
-     * Remove a file system listener.
-     *
-     * @param listener The file system listener
-     */
-    public void removeListener(final AdvancedFileAlterationListener listener) {
-        if (listener != null) {
-            while (listeners.remove(listener)) {
-            }
-        }
-    }
-
-    /**
-     * Returns the set of registered file system listeners.
-     *
-     * @return The file system listeners
-     */
-    public Iterable<AdvancedFileAlterationListener> getMagicListeners() {
-        return listeners;
-    }
-
-    /**
-     * Does nothing - hack for the monitor
-     *
-     *
-     */
-    public void initialize() {
-        // does nothing - hack the monitor
-    }
-
-
-    /**
-     * Initializes everything
-     *
-     * @throws Exception if an error occurs
-     */
-    public void init() throws Exception {
-        rootEntry.refresh(rootEntry.getFile());
-        final FileEntry[] children = doListFiles(rootEntry.getFile(), rootEntry);
-        rootEntry.setChildren(children);
-    }
-
-
-    /**
-     * Final processing.
-     *
-     * @throws Exception if an error occurs
-     */
-    public void destroy() throws Exception {
-        Iterator iterator = getMagicListeners().iterator();
-        while (iterator.hasNext()) {
-            AdvancedFileAlterationListener AdvancedFileAlterationListener = (AdvancedFileAlterationListener) iterator.next();
-            while (AdvancedFileAlterationListener.getActiveTasksCount() > 0) {
-                SystemClock.sleep(250);
-            }
-        }
-    }
-
-    public void checkAndNotifyNow() {
-                /* fire onStart() */
-        for (final AdvancedFileAlterationListener listener : listeners) {
-            listener.onStart(this);
-        }
-
-        /* fire directory/file events */
-        final File rootFile = rootEntry.getFile();
-        if (rootFile.exists()) {
-            checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile), 0);
-        } else if (rootEntry.isExists()) {
-            try {
-                // try to init once more
-                init();
-                if (rootEntry.getFile().exists()) {
-                    checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootEntry.getFile()), 0);
-                } else {
-                    checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY, 0);
-                }
-            } catch (Exception e) {
-                Log_OC.d("AdvancedFileAlterationObserver", "Failed getting an observer to intialize " + e);
-                checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY, 0);
-            }
-        } // else didn't exist and still doesn't
-
-        /* fire onStop() */
-        for (final AdvancedFileAlterationListener listener : listeners) {
-            listener.onStop(this);
-        }
-    }
-    
-    /**
-     * Check whether the file and its children have been created, modified or deleted.
-     */
-    public void checkAndNotify() {
-
-        /* fire onStart() */
-        for (final AdvancedFileAlterationListener listener : listeners) {
-            listener.onStart(this);
-        }
-
-        /* fire directory/file events */
-        final File rootFile = rootEntry.getFile();
-        if (rootFile.exists()) {
-            checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile), DELAY_INVOCATION_MS);
-        } else if (rootEntry.isExists()) {
-            try {
-                // try to init once more
-                init();
-                if (rootEntry.getFile().exists()) {
-                    checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootEntry.getFile()),
-                            DELAY_INVOCATION_MS);
-                } else {
-                    checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY, DELAY_INVOCATION_MS);
-                }
-            } catch (Exception e) {
-                Log_OC.d("AdvancedFileAlterationObserver", "Failed getting an observer to intialize " + e);
-                checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY, DELAY_INVOCATION_MS);
-            }
-        } // else didn't exist and still doesn't
-
-        /* fire onStop() */
-        for (final AdvancedFileAlterationListener listener : listeners) {
-            listener.onStop(this);
-        }
-    }
-
-    /**
-     * Compare two file lists for files which have been created, modified or deleted.
-     *
-     * @param parent   The parent entry
-     * @param previous The original list of files
-     * @param files    The current list of files
-     */
-    private void checkAndNotify(final FileEntry parent, final FileEntry[] previous, final File[] files, int delay) {
-        if (files != null && files.length > 0) {
-            int c = 0;
-            final FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : EMPTY_ENTRIES;
-            for (final FileEntry entry : previous) {
-                while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) {
-                    current[c] = createFileEntry(parent, files[c]);
-                    doCreate(current[c], delay);
-                    c++;
-                }
-                if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) {
-                    doMatch(entry, files[c], delay);
-                    checkAndNotify(entry, entry.getChildren(), listFiles(files[c]), delay);
-                    current[c] = entry;
-                    c++;
-                } else {
-                    checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY, delay);
-                    doDelete(entry);
-                }
-            }
-            for (; c < files.length; c++) {
-                current[c] = createFileEntry(parent, files[c]);
-                doCreate(current[c], delay);
-            }
-            parent.setChildren(current);
-        }
-    }
-
-    /**
-     * Create a new file entry for the specified file.
-     *
-     * @param parent The parent file entry
-     * @param file   The file to create an entry for
-     * @return A new file entry
-     */
-    private FileEntry createFileEntry(final FileEntry parent, final File file) {
-        final FileEntry entry = parent.newChildInstance(file);
-        entry.refresh(file);
-        final FileEntry[] children = doListFiles(file, entry);
-        entry.setChildren(children);
-        return entry;
-    }
-
-    /**
-     * List the files
-     *
-     * @param file  The file to list files for
-     * @param entry the parent entry
-     * @return The child files
-     */
-    private FileEntry[] doListFiles(File file, FileEntry entry) {
-        final File[] files = listFiles(file);
-        final FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : EMPTY_ENTRIES;
-        for (int i = 0; i < files.length; i++) {
-            children[i] = createFileEntry(entry, files[i]);
-        }
-        return children;
-    }
-
-    /**
-     * Fire directory/file created events to the registered listeners.
-     *
-     * @param entry The file entry
-     */
-    private void doCreate(final FileEntry entry, int delay) {
-        for (final AdvancedFileAlterationListener listener : listeners) {
-            if (entry.isDirectory()) {
-                listener.onDirectoryCreate(entry.getFile());
-            } else {
-                listener.onFileCreate(entry.getFile(), delay);
-            }
-        }
-        final FileEntry[] children = entry.getChildren();
-        for (final FileEntry aChildren : children) {
-            doCreate(aChildren, delay);
-        }
-    }
-
-    /**
-     * Fire directory/file change events to the registered listeners.
-     *
-     * @param entry The previous file system entry
-     * @param file  The current file
-     */
-    private void doMatch(final FileEntry entry, final File file, int delay) {
-        if (entry.refresh(file)) {
-            for (final AdvancedFileAlterationListener listener : listeners) {
-                if (entry.isDirectory()) {
-                    listener.onDirectoryChange(file);
-                } else {
-                    listener.onFileChange(file, delay);
-                }
-            }
-        }
-    }
-
-    /**
-     * Fire directory/file delete events to the registered listeners.
-     *
-     * @param entry The file entry
-     */
-    private void doDelete(final FileEntry entry) {
-        for (final AdvancedFileAlterationListener listener : listeners) {
-            if (entry.isDirectory()) {
-                listener.onDirectoryDelete(entry.getFile());
-            } else {
-                listener.onFileDelete(entry.getFile());
-            }
-        }
-    }
-
-    /**
-     * List the contents of a directory
-     *
-     * @param file The file to list the contents of
-     * @return the directory contents or a zero length array if
-     * the empty or the file is not a directory
-     */
-    private File[] listFiles(final File file) {
-        File[] children = null;
-        if (file.isDirectory()) {
-            children = fileFilter == null ? file.listFiles() : file.listFiles(fileFilter);
-        }
-        if (children == null) {
-            children = FileUtils.EMPTY_FILE_ARRAY;
-        }
-        if (comparator != null && children.length > 1) {
-            Arrays.sort(children, comparator);
-        }
-        return children;
-    }
-
-    /**
-     * Provide a String representation of this observer.
-     *
-     * @return a String representation of this observer
-     */
-    @Override
-    public String toString() {
-        final StringBuilder builder = new StringBuilder();
-        builder.append(getClass().getSimpleName());
-        builder.append("[file='");
-        builder.append(getDirectory().getPath());
-        builder.append('\'');
-        if (fileFilter != null) {
-            builder.append(", ");
-            builder.append(fileFilter.toString());
-        }
-        builder.append(", listeners=");
-        builder.append(listeners.size());
-        builder.append("]");
-        return builder.toString();
-    }
-
-}

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

@@ -1,180 +0,0 @@
-/**
- * Nextcloud Android client application
- *
- * @author Tobias Kaminsky
- * @author Andy Scherzinger
- * @author Mario Danic
- * Copyright (C) 2016 Tobias Kaminsky, Andy Scherzinger
- * Copyright (C) 2017 Mario Danic
- * <p>
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * at your option) any later version.
- * <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.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.R;
-import com.owncloud.android.datamodel.SyncedFolder;
-import com.owncloud.android.datamodel.SyncedFolderProvider;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.services.AdvancedFileAlterationListener;
-
-import org.apache.commons.io.monitor.FileAlterationMonitor;
-import org.apache.commons.io.monitor.FileAlterationObserver;
-
-import java.io.File;
-import java.io.FileFilter;
-
-public class SyncedFolderObserverService extends Service {
-    private static final String TAG = "SyncedFolderObserverService";
-
-    private static final int MONITOR_SCAN_INTERVAL = 1000;
-
-    private final IBinder mBinder = new SyncedFolderObserverBinder();
-    private FileAlterationMonitor monitor;
-    private FileFilter fileFilter;
-    
-    @Override
-    public void onCreate() {
-        SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(MainApp.getAppContext().
-                getContentResolver());
-        monitor = new FileAlterationMonitor(MONITOR_SCAN_INTERVAL);
-
-        fileFilter = new FileFilter() {
-            @Override
-            public boolean accept(File pathname) {
-                return !pathname.getName().startsWith(".") && !pathname.getName().endsWith(".tmp") &&
-                        !pathname.getName().endsWith(".temp") && !pathname.getName().endsWith(".thumbnail");
-            }
-        };
-
-
-        for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
-            if (syncedFolder.isEnabled()) {
-                AdvancedFileAlterationObserver observer = new AdvancedFileAlterationObserver(syncedFolder, fileFilter);
-
-                try {
-                    observer.init();
-                    observer.addListener(new AdvancedFileAlterationListener(syncedFolder,
-                            getResources().getBoolean(R.bool.syncedFolder_light)));
-                    monitor.addObserver(observer);
-                } catch (Exception e) {
-                    Log_OC.d(TAG, "Failed getting an observer to initialize " + e);
-                }
-
-            }
-        }
-
-
-        try {
-            monitor.start();
-        } catch (Exception e) {
-            Log_OC.d(TAG, "Something went very wrong at onStartCommand");
-        }
-
-    }
-
-    @Override
-    public void onDestroy() {
-
-        super.onDestroy();
-        for (FileAlterationObserver fileAlterationObserver : monitor.getObservers()) {
-            AdvancedFileAlterationObserver advancedFileAlterationObserver = (AdvancedFileAlterationObserver)
-                    fileAlterationObserver;
-            try {
-                monitor.removeObserver(advancedFileAlterationObserver);
-                advancedFileAlterationObserver.checkAndNotifyNow();
-                advancedFileAlterationObserver.destroy();
-            } catch (Exception e) {
-                Log_OC.d(TAG, "Something went very wrong on trying to destroy observers");
-            }
-        }
-    }
-
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        return Service.START_NOT_STICKY;
-    }
-
-    /**
-     * Restart oberver if it is enabled
-     * If syncedFolder exists already, use it, otherwise create new observer
-     *
-     * @param syncedFolder
-     */
-
-    public void restartObserver(SyncedFolder syncedFolder) {
-        boolean found = false;
-        AdvancedFileAlterationObserver advancedFileAlterationObserver;
-        for (FileAlterationObserver fileAlterationObserver : monitor.getObservers()) {
-            advancedFileAlterationObserver =
-                    (AdvancedFileAlterationObserver) fileAlterationObserver;
-            if (advancedFileAlterationObserver.getSyncedFolderID() == syncedFolder.getId()) {
-                monitor.removeObserver(fileAlterationObserver);
-                advancedFileAlterationObserver.checkAndNotifyNow();
-                try {
-                    advancedFileAlterationObserver.destroy();
-                } catch (Exception e) {
-                    Log_OC.d(TAG, "Failed to destroy the observer in restart");
-                }
-
-                if (syncedFolder.isEnabled()) {
-                    try {
-                        advancedFileAlterationObserver = new AdvancedFileAlterationObserver(syncedFolder, fileFilter);
-                        advancedFileAlterationObserver.init();
-                        advancedFileAlterationObserver.addListener(new AdvancedFileAlterationListener(syncedFolder,
-                                getResources().getBoolean(R.bool.syncedFolder_light)));
-                        monitor.addObserver(advancedFileAlterationObserver);
-                    } catch (Exception e) {
-                        Log_OC.d(TAG, "Failed getting an observer to initialize");
-                    }
-                } else {
-                    monitor.removeObserver(fileAlterationObserver);
-                }
-                found = true;
-                break;
-            }
-        }
-
-        if (!found && syncedFolder.isEnabled()) {
-            try {
-                advancedFileAlterationObserver = new AdvancedFileAlterationObserver(syncedFolder, fileFilter);
-                advancedFileAlterationObserver.init();
-                advancedFileAlterationObserver.addListener(new AdvancedFileAlterationListener(syncedFolder,
-                        getResources().getBoolean(R.bool.syncedFolder_light)));
-                monitor.addObserver(advancedFileAlterationObserver);
-            } catch (Exception e) {
-                Log_OC.d(TAG, "Failed getting an observer to initialize");
-            }
-
-        }
-
-    }
-
-    @Override
-    public IBinder onBind(Intent arg0) {
-        return mBinder;
-    }
-
-    public class SyncedFolderObserverBinder extends Binder {
-        public SyncedFolderObserverService getService() {
-            return SyncedFolderObserverService.this;
-        }
-    }
-
-}

+ 1 - 1
src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java

@@ -37,7 +37,7 @@ import com.evernote.android.job.util.support.PersistableBundleCompat;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.services.ContactsBackupJob;
+import com.owncloud.android.jobs.ContactsBackupJob;
 import com.owncloud.android.ui.fragment.FileFragment;
 import com.owncloud.android.ui.fragment.contactsbackup.ContactListFragment;
 import com.owncloud.android.ui.fragment.contactsbackup.ContactsBackupFragment;

+ 4 - 10
src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java

@@ -211,12 +211,6 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
             setupDrawerMenu(mNavigationView);
 
             setupQuotaElement();
-
-            // show folder sync menu item only for Android 6+
-            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M &&
-                    mNavigationView.getMenu().findItem(R.id.nav_folder_sync) != null) {
-                mNavigationView.getMenu().removeItem(R.id.nav_folder_sync);
-            }
         }
 
         setupDrawerToggle();
@@ -366,7 +360,7 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
         }
 
         if (getResources().getBoolean(R.bool.syncedFolder_light)) {
-            navigationView.getMenu().removeItem(R.id.nav_folder_sync);
+            navigationView.getMenu().removeItem(R.id.nav_synced_folders);
         }
 
         if (!getResources().getBoolean(R.bool.show_drawer_logout)) {
@@ -450,9 +444,9 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
                 Intent notificationsIntent = new Intent(getApplicationContext(), NotificationsActivity.class);
                 startActivity(notificationsIntent);
                 break;
-            case R.id.nav_folder_sync:
-                Intent folderSyncIntent = new Intent(getApplicationContext(), FolderSyncActivity.class);
-                startActivity(folderSyncIntent);
+            case R.id.nav_synced_folders:
+                Intent syncedFoldersIntent = new Intent(getApplicationContext(), SyncedFoldersActivity.class);
+                startActivity(syncedFoldersIntent);
                 break;
             case R.id.nav_contacts:
                 Intent contactsIntent = new Intent(getApplicationContext(), ContactsPreferenceActivity.class);

+ 2 - 3
src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java

@@ -21,7 +21,6 @@
 
 package com.owncloud.android.ui.activity;
 
-import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
 import android.support.v4.widget.DrawerLayout;
@@ -113,7 +112,6 @@ public class ExternalSiteWebView extends FileActivity {
         webSettings.setJavaScriptEnabled(true);
         webSettings.setDomStorageEnabled(true);
 
-        final Activity activity = this;
         final ProgressBar progressBar = (ProgressBar) findViewById(R.id.progressBar);
 
         webview.setWebChromeClient(new WebChromeClient() {
@@ -124,7 +122,8 @@ public class ExternalSiteWebView extends FileActivity {
 
         webview.setWebViewClient(new WebViewClient() {
             public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
-                webview.loadData(DisplayUtils.getData(getResources().openRawResource(R.raw.custom_error)),"text/html; charset=UTF-8", null);
+                webview.loadData(DisplayUtils.getData(getResources().openRawResource(R.raw.custom_error)),
+                        "text/html; charset=UTF-8", null);
             }
         });
 

+ 1 - 1
src/main/java/com/owncloud/android/ui/activity/FileActivity.java

@@ -476,7 +476,7 @@ public abstract class FileActivity extends DrawerActivity
         Fragment frag = getSupportFragmentManager().findFragmentByTag(DIALOG_WAIT_TAG);
         if (frag == null) {
             Log_OC.d(TAG, "show loading dialog");
-            LoadingDialog loading = new LoadingDialog(message);
+            LoadingDialog loading = LoadingDialog.newInstance(message);
             FragmentManager fm = getSupportFragmentManager();
             FragmentTransaction ft = fm.beginTransaction();
             loading.show(ft, DIALOG_WAIT_TAG);

+ 11 - 10
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -282,9 +282,8 @@ public class FileDisplayActivity extends HookActivity
      */
     private void upgradeNotificationForInstantUpload() {
         // check for Android 6+ if legacy instant upload is activated --> disable + show info
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
-                (PreferenceManager.instantPictureUploadEnabled(this) ||
-                        PreferenceManager.instantPictureUploadEnabled(this))) {
+        if (PreferenceManager.instantPictureUploadEnabled(this) ||
+                        PreferenceManager.instantPictureUploadEnabled(this)) {
 
             // remove legacy shared preferences
             SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
@@ -303,14 +302,14 @@ public class FileDisplayActivity extends HookActivity
 
             // 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)
+                    .setTitle(R.string.drawer_synced_folders)
+                    .setMessage(R.string.synced_folders_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);
+                            Intent syncedFoldersIntent = new Intent(getApplicationContext(), SyncedFoldersActivity.class);
                             dialog.dismiss();
-                            startActivity(folderSyncIntent);
+                            startActivity(syncedFoldersIntent);
                         }
                     })
                     .setNegativeButton(R.string.drawer_close, new DialogInterface.OnClickListener() {
@@ -318,7 +317,7 @@ public class FileDisplayActivity extends HookActivity
                             dialog.dismiss();
                         }
                     })
-                    .setIcon(R.drawable.nav_folder_sync)
+                    .setIcon(R.drawable.nav_synced_folders)
                     .show();
         }
     }
@@ -477,7 +476,7 @@ public class FileDisplayActivity extends HookActivity
         super.onNewIntent(intent);
         if(intent.getAction()!=null && intent.getAction().equalsIgnoreCase(ACTION_DETAILS)){
             setIntent(intent);
-            setFile((OCFile)intent.getParcelableExtra(EXTRA_FILE));
+            setFile(intent.getParcelableExtra(EXTRA_FILE));
         }
     }
 
@@ -915,7 +914,9 @@ public class FileDisplayActivity extends HookActivity
                     null,           // MIME type will be detected from file name
                     behaviour,
                     false,          // do not create parent folder if not existent
-                    UploadFileOperation.CREATED_BY_USER
+                    UploadFileOperation.CREATED_BY_USER,
+                    false,
+                    false
             );
 
         } else {

+ 1 - 2
src/main/java/com/owncloud/android/ui/activity/FingerprintActivity.java

@@ -77,7 +77,6 @@ public class FingerprintActivity extends AppCompatActivity {
 
     public final static String KEY_CHECK_RESULT = "KEY_CHECK_RESULT";
 
-    public final static String PREFERENCE_USE_FINGERPRINT = "use_fingerprint";
     public static final String ANDROID_KEY_STORE = "AndroidKeyStore";
 
     private KeyStore keyStore;
@@ -286,7 +285,7 @@ public class FingerprintActivity extends AppCompatActivity {
     }
 }
 
-@RequiresApi(Build.VERSION_CODES.M)
+@RequiresApi(api = Build.VERSION_CODES.M)
 class FingerprintHandler extends FingerprintManager.AuthenticationCallback {
 
     private TextView text;

+ 1 - 0
src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.java

@@ -302,6 +302,7 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C
             retval = super.onOptionsItemSelected(item);
             break;
         }
+
         return retval;
     }
 

+ 1 - 3
src/main/java/com/owncloud/android/ui/activity/LogHistoryActivity.java

@@ -273,9 +273,7 @@ public class LogHistoryActivity extends ToolbarActivity {
      */
     public void showLoadingDialog() {
         // Construct dialog
-        LoadingDialog loading = new LoadingDialog(
-                getResources().getString(R.string.log_progress_dialog_text)
-        );
+        LoadingDialog loading = LoadingDialog.newInstance(getResources().getString(R.string.log_progress_dialog_text));
         FragmentManager fm = getSupportFragmentManager();
         FragmentTransaction ft = fm.beginTransaction();
         loading.show(ft, DIALOG_WAIT_TAG);

+ 3 - 4
src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java

@@ -50,8 +50,7 @@ import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.services.AccountRemovalJob;
-import com.owncloud.android.services.AutoUploadJob;
+import com.owncloud.android.jobs.AccountRemovalJob;
 import com.owncloud.android.services.OperationsService;
 import com.owncloud.android.ui.adapter.AccountListAdapter;
 import com.owncloud.android.ui.adapter.AccountListItem;
@@ -407,7 +406,7 @@ public class ManageAccountsActivity extends FileActivity
 
         // store pending account removal
         ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(getContentResolver());
-        arbitraryDataProvider.storeOrUpdateKeyValue(account, PENDING_FOR_REMOVAL, String.valueOf(true));
+        arbitraryDataProvider.storeOrUpdateKeyValue(account.name, PENDING_FOR_REMOVAL, String.valueOf(true));
 
         // Cancel transfers
         if (mUploaderBinder != null) {
@@ -419,7 +418,7 @@ public class ManageAccountsActivity extends FileActivity
 
         // schedule job
         PersistableBundleCompat bundle = new PersistableBundleCompat();
-        bundle.putString(AutoUploadJob.ACCOUNT, account.name);
+        bundle.putString(AccountRemovalJob.ACCOUNT, account.name);
 
         new JobRequest.Builder(AccountRemovalJob.TAG)
                 .setExecutionWindow(1_000L, 10_000L)

+ 54 - 150
src/main/java/com/owncloud/android/ui/activity/Preferences.java

@@ -85,9 +85,11 @@ import java.io.IOException;
  */
 public class Preferences extends PreferenceActivity
         implements StorageMigration.StorageMigrationProgressListener {
-    
+
     private static final String TAG = Preferences.class.getSimpleName();
 
+    public final static String PREFERENCE_USE_FINGERPRINT = "use_fingerprint";
+
     private static final String SCREEN_NAME = "Settings";
 
     private static final int ACTION_SELECT_UPLOAD_PATH = 1;
@@ -107,6 +109,7 @@ public class Preferences extends PreferenceActivity
     private SwitchPreference pCode;
     private SwitchPreference fPrint;
     private SwitchPreference mShowHiddenFiles;
+    private SwitchPreference mExpertMode;
     private Preference pAboutApp;
     private AppCompatDelegate mDelegate;
 
@@ -190,13 +193,14 @@ public class Preferences extends PreferenceActivity
                 accentColor));
 
         // Synced folders
-        PreferenceCategory preferenceCategoryFolderSync = (PreferenceCategory) findPreference("folder_sync");
-        preferenceCategoryFolderSync.setTitle(ThemeUtils.getColoredTitle(getString(R.string.drawer_folder_sync),
+        PreferenceCategory preferenceCategorySyncedFolders =
+                (PreferenceCategory) findPreference("synced_folders_category");
+        preferenceCategorySyncedFolders.setTitle(ThemeUtils.getColoredTitle(getString(R.string.drawer_synced_folders),
                 accentColor));
         PreferenceScreen preferenceScreen = (PreferenceScreen) findPreference("preference_screen");
 
         if (!getResources().getBoolean(R.bool.syncedFolder_light)) {
-            preferenceScreen.removePreference(preferenceCategoryFolderSync);
+            preferenceScreen.removePreference(preferenceCategorySyncedFolders);
         } else {
             // Upload on WiFi
             final ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(getContentResolver());
@@ -209,28 +213,28 @@ public class Preferences extends PreferenceActivity
             pUploadOnWifiCheckbox.setOnPreferenceClickListener(new OnPreferenceClickListener() {
                 @Override
                 public boolean onPreferenceClick(Preference preference) {
-                    arbitraryDataProvider.storeOrUpdateKeyValue(account, SYNCED_FOLDER_LIGHT_UPLOAD_ON_WIFI,
+                    arbitraryDataProvider.storeOrUpdateKeyValue(account.name, SYNCED_FOLDER_LIGHT_UPLOAD_ON_WIFI,
                             String.valueOf(pUploadOnWifiCheckbox.isChecked()));
 
                     return true;
                 }
             });
 
-            Preference pSyncedFolder = findPreference("folder_sync_folders");
+            Preference pSyncedFolder = findPreference("synced_folders_configure_folders");
             if (pSyncedFolder != null) {
                 if (getResources().getBoolean(R.bool.syncedFolder_light)
                         && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                     pSyncedFolder.setOnPreferenceClickListener(new OnPreferenceClickListener() {
                         @Override
                         public boolean onPreferenceClick(Preference preference) {
-                            Intent folderSyncIntent = new Intent(getApplicationContext(), FolderSyncActivity.class);
-                            folderSyncIntent.putExtra(FolderSyncActivity.EXTRA_SHOW_SIDEBAR, false);
-                            startActivity(folderSyncIntent);
+                            Intent syncedFoldersIntent = new Intent(getApplicationContext(), SyncedFoldersActivity.class);
+                            syncedFoldersIntent.putExtra(SyncedFoldersActivity.EXTRA_SHOW_SIDEBAR, false);
+                            startActivity(syncedFoldersIntent);
                             return true;
                         }
                     });
                 } else {
-                    preferenceCategoryFolderSync.removePreference(pSyncedFolder);
+                    preferenceCategorySyncedFolders.removePreference(pSyncedFolder);
                 }
             }
         }
@@ -265,7 +269,7 @@ public class Preferences extends PreferenceActivity
         }
 
         boolean fPrintEnabled = getResources().getBoolean(R.bool.fingerprint_enabled);
-        fPrint = (SwitchPreference) findPreference(FingerprintActivity.PREFERENCE_USE_FINGERPRINT);
+        fPrint = (SwitchPreference) findPreference(PREFERENCE_USE_FINGERPRINT);
         if (fPrint != null) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                 if (FingerprintActivity.isFingerprintCapable(MainApp.getAppContext()) && fPrintEnabled) {
@@ -332,9 +336,21 @@ public class Preferences extends PreferenceActivity
             });
         } else {
             preferenceCategoryDetails.removePreference(mShowHiddenFiles);
-
         }
 
+        mExpertMode = (SwitchPreference) findPreference("expert_mode");
+        mExpertMode.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+            @Override
+            public boolean onPreferenceClick(Preference preference) {
+                SharedPreferences appPrefs =
+                        PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+                SharedPreferences.Editor editor = appPrefs.edit();
+                editor.putBoolean("expert_mode", mExpertMode.isChecked());
+                editor.apply();
+                return true;
+            }
+        });
+
         PreferenceCategory preferenceCategoryMore = (PreferenceCategory) findPreference("more");
         preferenceCategoryMore.setTitle(ThemeUtils.getColoredTitle(getString(R.string.prefs_category_more),
                 accentColor));
@@ -468,7 +484,11 @@ public class Preferences extends PreferenceActivity
             }
         }
 
-        boolean loggerEnabled = getResources().getBoolean(R.bool.logger_enabled) || BuildConfig.DEBUG;
+        SharedPreferences appPrefs =
+                PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+
+        boolean loggerEnabled = getResources().getBoolean(R.bool.logger_enabled) || BuildConfig.DEBUG ||
+                appPrefs.getBoolean("expert_mode", false);
         Preference pLogger = findPreference("logger");
         if (pLogger != null) {
             if (loggerEnabled) {
@@ -521,12 +541,12 @@ public class Preferences extends PreferenceActivity
             mPrefStoragePath.setEntryValues(values);
 
             mPrefStoragePath.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
-                    @Override
-                    public boolean onPreferenceChange(Preference preference, Object newValue) {
-                        String newPath = (String) newValue;
-                        if (mStoragePath.equals(newPath)) {
-                            return true;
-                        }
+                @Override
+                public boolean onPreferenceChange(Preference preference, Object newValue) {
+                    String newPath = (String) newValue;
+                    if (mStoragePath.equals(newPath)) {
+                        return true;
+                    }
 
                     StorageMigration storageMigration = new StorageMigration(Preferences.this, mStoragePath, newPath);
 
@@ -540,89 +560,7 @@ public class Preferences extends PreferenceActivity
         }
 
         mPrefInstantUploadCategory = (PreferenceCategory) findPreference("instant_uploading_category");
-
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
-            // Instant upload via preferences on pre Android Marshmallow
-            mPrefInstantUploadPath = findPreference("instant_upload_path");
-            if (mPrefInstantUploadPath != null) {
-
-                mPrefInstantUploadPath.setOnPreferenceClickListener(new OnPreferenceClickListener() {
-                    @Override
-                    public boolean onPreferenceClick(Preference preference) {
-                        if (!mUploadPath.endsWith(OCFile.PATH_SEPARATOR)) {
-                            mUploadPath += OCFile.PATH_SEPARATOR;
-                        }
-                        Intent intent = new Intent(Preferences.this, UploadPathActivity.class);
-                        intent.putExtra(UploadPathActivity.KEY_INSTANT_UPLOAD_PATH, mUploadPath);
-                        startActivityForResult(intent, ACTION_SELECT_UPLOAD_PATH);
-                        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 = (CheckBoxPreferenceWithLongTitle) findPreference("instant_uploading");
-
-            toggleInstantPictureOptions(mPrefInstantUpload.isChecked());
-
-            mPrefInstantUpload.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
-                @Override
-                public boolean onPreferenceChange(Preference preference, Object newValue) {
-                    toggleInstantPictureOptions((Boolean) newValue);
-                    toggleInstantUploadBehaviour(mPrefInstantVideoUpload.isChecked(), (Boolean) newValue);
-                    return true;
-                }
-            });
-
-        mPrefInstantVideoUploadPath = findPreference(PreferenceKeys.INSTANT_VIDEO_UPLOAD_PATH);
-            if (mPrefInstantVideoUploadPath != null) {
-
-                mPrefInstantVideoUploadPath.setOnPreferenceClickListener(new OnPreferenceClickListener() {
-                    @Override
-                    public boolean onPreferenceClick(Preference preference) {
-                        if (!mUploadVideoPath.endsWith(OCFile.PATH_SEPARATOR)) {
-                            mUploadVideoPath += OCFile.PATH_SEPARATOR;
-                        }
-                        Intent intent = new Intent(Preferences.this, UploadPathActivity.class);
-                        intent.putExtra(UploadPathActivity.KEY_INSTANT_UPLOAD_PATH,
-                                mUploadVideoPath);
-                        startActivityForResult(intent, ACTION_SELECT_UPLOAD_VIDEO_PATH);
-                        return true;
-                    }
-                });
-            }
-
-            mPrefInstantVideoUploadUseSubfolders = findPreference("instant_video_upload_path_use_subfolders");
-            mPrefInstantVideoUploadPathWiFi = findPreference("instant_video_upload_on_wifi");
-            mPrefInstantVideoUpload = (CheckBoxPreferenceWithLongTitle) findPreference("instant_video_uploading");
-            mPrefInstantVideoUploadOnlyOnCharging = findPreference("instant_video_upload_on_charging");
-            toggleInstantVideoOptions(mPrefInstantVideoUpload.isChecked());
-            mPrefInstantVideoUpload.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
-                @Override
-                public boolean onPreferenceChange(Preference preference, Object newValue) {
-                    toggleInstantVideoOptions((Boolean) newValue);
-                    toggleInstantUploadBehaviour(
-                            (Boolean) newValue,
-                            mPrefInstantUpload.isChecked());
-                    return true;
-                }
-            });
-
-            mPrefInstantUploadBehaviour = findPreference("prefs_instant_behaviour");
-            toggleInstantUploadBehaviour(
-                    mPrefInstantVideoUpload.isChecked(),
-                    mPrefInstantUpload.isChecked());
-
-            loadInstantUploadPath();
-            loadInstantUploadVideoPath();
-        } else {
-            // Instant upload is handled via synced folders on Android Lollipop and up
-            getPreferenceScreen().removePreference(mPrefInstantUploadCategory);
-        }
+        getPreferenceScreen().removePreference(mPrefInstantUploadCategory);
 
         // About category
         PreferenceCategory preferenceCategoryAbout = (PreferenceCategory) findPreference("about");
@@ -716,48 +654,12 @@ public class Preferences extends PreferenceActivity
                     mUri = OwnCloudClientManagerFactory.getDefaultSingleton().
                             getClientFor(ocAccount, getApplicationContext()).getBaseUri();
                 } catch (Throwable t) {
-                    Log_OC.e(TAG,"Error retrieving user's base URI", t);
+                    Log_OC.e(TAG, "Error retrieving user's base URI", t);
                 }
             }
         });
         t.start();
     }
-    
-    private void toggleInstantPictureOptions(Boolean value){
-        if (value) {
-            mPrefInstantUploadCategory.addPreference(mPrefInstantUploadPathWiFi);
-            mPrefInstantUploadCategory.addPreference(mPrefInstantUploadPath);
-            mPrefInstantUploadCategory.addPreference(mPrefInstantUploadUseSubfolders);
-            mPrefInstantUploadCategory.addPreference(mPrefInstantPictureUploadOnlyOnCharging);
-        } else {
-            mPrefInstantUploadCategory.removePreference(mPrefInstantUploadPathWiFi);
-            mPrefInstantUploadCategory.removePreference(mPrefInstantUploadPath);
-            mPrefInstantUploadCategory.removePreference(mPrefInstantUploadUseSubfolders);
-            mPrefInstantUploadCategory.removePreference(mPrefInstantPictureUploadOnlyOnCharging);
-        }
-    }
-
-    private void toggleInstantVideoOptions(Boolean value){
-        if (value) {
-            mPrefInstantUploadCategory.addPreference(mPrefInstantVideoUploadPathWiFi);
-            mPrefInstantUploadCategory.addPreference(mPrefInstantVideoUploadPath);
-            mPrefInstantUploadCategory.addPreference(mPrefInstantVideoUploadUseSubfolders);
-            mPrefInstantUploadCategory.addPreference(mPrefInstantVideoUploadOnlyOnCharging);
-        } else {
-            mPrefInstantUploadCategory.removePreference(mPrefInstantVideoUploadPathWiFi);
-            mPrefInstantUploadCategory.removePreference(mPrefInstantVideoUploadPath);
-            mPrefInstantUploadCategory.removePreference(mPrefInstantVideoUploadUseSubfolders);
-            mPrefInstantUploadCategory.removePreference(mPrefInstantVideoUploadOnlyOnCharging);
-        }
-    }
-
-    private void toggleInstantUploadBehaviour(Boolean video, Boolean picture){
-        if (picture || video) {
-            mPrefInstantUploadCategory.addPreference(mPrefInstantUploadBehaviour);
-        } else {
-            mPrefInstantUploadCategory.removePreference(mPrefInstantUploadBehaviour);
-        }
-    }
 
     @Override
     protected void onResume() {
@@ -782,14 +684,14 @@ public class Preferences extends PreferenceActivity
         Intent intent;
 
         switch (item.getItemId()) {
-        case android.R.id.home:
-            intent = new Intent(getBaseContext(), FileDisplayActivity.class);
-            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-            startActivity(intent);
-            break;
-        default:
-            Log_OC.w(TAG, "Unknown menu item triggered");
-            return false;
+            case android.R.id.home:
+                intent = new Intent(getBaseContext(), FileDisplayActivity.class);
+                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                startActivity(intent);
+                break;
+            default:
+                Log_OC.w(TAG, "Unknown menu item triggered");
+                return false;
         }
         return true;
     }
@@ -800,7 +702,7 @@ public class Preferences extends PreferenceActivity
 
         if (requestCode == ACTION_SELECT_UPLOAD_PATH && resultCode == RESULT_OK) {
 
-            OCFile folderToUpload =  data.getParcelableExtra(UploadPathActivity.EXTRA_FOLDER);
+            OCFile folderToUpload = data.getParcelableExtra(UploadPathActivity.EXTRA_FOLDER);
 
             mUploadPath = folderToUpload.getRemotePath();
 
@@ -830,7 +732,7 @@ public class Preferences extends PreferenceActivity
                         .getDefaultSharedPreferences(getApplicationContext()).edit();
 
                 for (int i = 1; i <= 4; ++i) {
-                    appPrefs.putString(PassCodeActivity.PREFERENCE_PASSCODE_D + i, passcode.substring(i-1, i));
+                    appPrefs.putString(PassCodeActivity.PREFERENCE_PASSCODE_D + i, passcode.substring(i - 1, i));
                 }
                 appPrefs.putBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, true);
                 appPrefs.apply();
@@ -868,10 +770,12 @@ public class Preferences extends PreferenceActivity
     public void setContentView(@LayoutRes int layoutResID) {
         getDelegate().setContentView(layoutResID);
     }
+
     @Override
     public void setContentView(View view) {
         getDelegate().setContentView(view);
     }
+
     @Override
     public void setContentView(View view, ViewGroup.LayoutParams params) {
         getDelegate().setContentView(view, params);
@@ -994,7 +898,7 @@ public class Preferences extends PreferenceActivity
         SharedPreferences appPrefs =
                 PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
         mStoragePath = appPrefs.getString(PreferenceKeys.STORAGE_PATH, Environment.getExternalStorageDirectory()
-                                                         .getAbsolutePath());
+                .getAbsolutePath());
         String storageDescription = DataStorageProvider.getInstance().getStorageDescriptionByPath(mStoragePath);
         mPrefStoragePath.setSummary(storageDescription);
     }

+ 3 - 1
src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java

@@ -913,7 +913,9 @@ public class ReceiveExternalFilesActivity extends FileActivity
             FileUploader.LOCAL_BEHAVIOUR_COPY,
             null,
             true,
-            UploadFileOperation.CREATED_BY_USER
+            UploadFileOperation.CREATED_BY_USER,
+            false,
+            false
             );
         finish();
     }

+ 4 - 16
src/main/java/com/owncloud/android/ui/activity/ShareActivity.java

@@ -55,11 +55,9 @@ import com.owncloud.android.utils.GetShareWithUsersAsyncTask;
 import java.util.ArrayList;
 
 /**
- * Activity for sharing files
+ * Activity for sharing files.
  */
-
-public class ShareActivity extends FileActivity
-        implements ShareFragmentListener {
+public class ShareActivity extends FileActivity implements ShareFragmentListener {
 
     private static final String TAG = ShareActivity.class.getSimpleName();
 
@@ -70,8 +68,6 @@ public class ShareActivity extends FileActivity
 
     /// Tags for dialog fragments
     private static final String FTAG_CHOOSER_DIALOG = "CHOOSER_DIALOG";
-    private static final String FTAG_SHARE_PASSWORD_DIALOG = "SHARE_PASSWORD_DIALOG";
-
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -87,7 +83,6 @@ public class ShareActivity extends FileActivity
             ft.replace(R.id.share_fragment_container, fragment, TAG_SHARE_FRAGMENT);
             ft.commit();
         }
-
     }
 
     protected void onAccountSet(boolean stateWasRecovered) {
@@ -144,10 +139,9 @@ public class ShareActivity extends FileActivity
         );
     }
 
-
     private int getAppropiatePermissions(ShareType shareType) {
 
-        // check if the Share is FERERATED
+        // check if the Share is FEDERATED
         boolean isFederated = ShareType.FEDERATED.equals(shareType);
 
         if (getFile().isSharedWithMe()) {
@@ -245,9 +239,8 @@ public class ShareActivity extends FileActivity
 
     }
 
-
     /**
-     * Updates the view, reading data from {@link com.owncloud.android.datamodel.FileDataStorageManager}
+     * Updates the view, reading data from {@link com.owncloud.android.datamodel.FileDataStorageManager}.
      */
     private void refreshSharesFromStorageManager() {
 
@@ -270,7 +263,6 @@ public class ShareActivity extends FileActivity
                 editShareFragment.isAdded()) {
             editShareFragment.refreshUiFromDB();
         }
-
     }
 
     /**
@@ -300,7 +292,6 @@ public class ShareActivity extends FileActivity
         return (EditShareFragment) getSupportFragmentManager().findFragmentByTag(TAG_EDIT_SHARE_FRAGMENT);
     }
 
-
     private void onCreateShareViaLinkOperationFinish(CreateShareViaLinkOperation operation,
                                                      RemoteOperationResult result) {
         if (result.isSuccess()) {
@@ -369,8 +360,5 @@ public class ShareActivity extends FileActivity
                 t.show();
             }
         }
-
     }
-
-
 }

+ 208 - 67
src/main/java/com/owncloud/android/ui/activity/FolderSyncActivity.java → src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java

@@ -23,56 +23,66 @@ package com.owncloud.android.ui.activity;
 
 import android.accounts.Account;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.os.Bundle;
-import android.os.Handler;
+import android.preference.PreferenceManager;
 import android.support.annotation.NonNull;
+import android.support.design.widget.AppBarLayout;
 import android.support.design.widget.BottomNavigationView;
+import android.support.design.widget.CollapsingToolbarLayout;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.FragmentTransaction;
 import android.support.v4.widget.DrawerLayout;
 import android.support.v7.app.ActionBar;
 import android.support.v7.widget.GridLayoutManager;
 import android.support.v7.widget.RecyclerView;
+import android.util.Log;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
 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.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.MediaFolder;
+import com.owncloud.android.datamodel.MediaFolderType;
 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.files.services.FileUploader;
-import com.owncloud.android.ui.adapter.FolderSyncAdapter;
+import com.owncloud.android.ui.adapter.SyncedFolderAdapter;
 import com.owncloud.android.ui.decoration.MediaGridItemDecoration;
 import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment;
 import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable;
 import com.owncloud.android.utils.AnalyticsUtils;
 import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.FilesSyncHelper;
 import com.owncloud.android.utils.PermissionUtil;
 import com.owncloud.android.utils.ThemeUtils;
 
 import java.io.File;
+import java.io.FileFilter;
 import java.util.ArrayList;
+import java.util.Arrays;
 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 android.support.design.widget.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS;
 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,
+public class SyncedFoldersActivity extends FileActivity implements SyncedFolderAdapter.ClickListener,
         SyncedFolderPreferencesDialogFragment.OnSyncedFolderPreferenceListener {
 
     private static final String SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG";
@@ -81,16 +91,16 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
 
     private static final String SCREEN_NAME = "Auto upload";
 
-    private static final String TAG = FolderSyncActivity.class.getSimpleName();
+    private static final String TAG = SyncedFoldersActivity.class.getSimpleName();
 
     private RecyclerView mRecyclerView;
-    private FolderSyncAdapter mAdapter;
+    private SyncedFolderAdapter mAdapter;
     private LinearLayout mProgress;
     private TextView mEmpty;
     private SyncedFolderProvider mSyncedFolderProvider;
-    private List<SyncedFolderDisplayItem> syncFolderItems;
     private SyncedFolderPreferencesDialogFragment mSyncedFolderPreferencesDialogFragment;
     private boolean showSidebar = true;
+    private RelativeLayout mCustomFolderRelativeLayout;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -100,13 +110,37 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
             showSidebar = getIntent().getExtras().getBoolean(EXTRA_SHOW_SIDEBAR);
         }
 
-        setContentView(R.layout.folder_sync_layout);
+        setContentView(R.layout.synced_folders_layout);
 
         // setup toolbar
         setupToolbar();
+        CollapsingToolbarLayout mCollapsingToolbarLayout = ((CollapsingToolbarLayout)
+                findViewById(R.id.collapsing_toolbar));
+        mCollapsingToolbarLayout.setTitle(this.getString(R.string.drawer_synced_folders));
+
+        mCustomFolderRelativeLayout = (RelativeLayout) findViewById(R.id.custom_folder_toolbar);
+
+        SharedPreferences appPrefs =
+                PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+
+
+        findViewById(R.id.toolbar).post(() -> {
+            if (!appPrefs.getBoolean("expert_mode", false)) {
+                findViewById(R.id.app_bar).getLayoutParams().height = findViewById(R.id.toolbar).getHeight();
+
+                AppBarLayout.LayoutParams p = (AppBarLayout.LayoutParams) mCollapsingToolbarLayout.getLayoutParams();
+                p.setScrollFlags(SCROLL_FLAG_ENTER_ALWAYS);
+                mCollapsingToolbarLayout.setLayoutParams(p);
+                mCustomFolderRelativeLayout.setVisibility(View.GONE);
+            } else {
+                mCustomFolderRelativeLayout.setVisibility(View.VISIBLE);
+                findViewById(R.id.app_bar).setBackgroundColor(getResources().getColor(R.color.filelist_icon_backgorund));
+            }
+        });
+
 
         // setup drawer
-        setupDrawer(R.id.nav_folder_sync);
+        setupDrawer(R.id.nav_synced_folders);
 
         if (!showSidebar) {
             setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
@@ -117,7 +151,7 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
 
         ActionBar actionBar = getSupportActionBar();
         if (actionBar != null) {
-            ThemeUtils.setColoredTitle(getSupportActionBar(), getString(R.string.drawer_folder_sync));
+            ThemeUtils.setColoredTitle(getSupportActionBar(), getString(R.string.drawer_synced_folders));
             actionBar.setDisplayHomeAsUpEnabled(true);
         }
 
@@ -143,7 +177,7 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
 
         final int gridWidth = getResources().getInteger(R.integer.media_grid_width);
         boolean lightVersion = getResources().getBoolean(R.bool.syncedFolder_light);
-        mAdapter = new FolderSyncAdapter(this, gridWidth, this, lightVersion);
+        mAdapter = new SyncedFolderAdapter(this, gridWidth, this, lightVersion);
         mSyncedFolderProvider = new SyncedFolderProvider(getContentResolver());
 
         final GridLayoutManager lm = new GridLayoutManager(this, gridWidth);
@@ -173,33 +207,25 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
             return;
         }
         setListShown(false);
-        final Handler mHandler = new Handler();
-        new Thread(new Runnable() {
-            @Override
-            public void run() {
-                final List<MediaFolder> mediaFolders = MediaProvider.getMediaFolders(getContentResolver(),
-                        perFolderMediaItemLimit, FolderSyncActivity.this);
-                List<SyncedFolder> syncedFolderArrayList = mSyncedFolderProvider.getSyncedFolders();
-                List<SyncedFolder> currentAccountSyncedFoldersList = new ArrayList<>();
-                Account currentAccount = AccountUtils.getCurrentOwnCloudAccount(FolderSyncActivity.this);
-                for (SyncedFolder syncedFolder : syncedFolderArrayList) {
-                    if (syncedFolder.getAccount().equals(currentAccount.name)) {
-                        currentAccountSyncedFoldersList.add(syncedFolder);
-                    }
-                }
+        final List<MediaFolder> mediaFolders = MediaProvider.getImageFolders(getContentResolver(),
+                perFolderMediaItemLimit, SyncedFoldersActivity.this);
+        mediaFolders.addAll(MediaProvider.getVideoFolders(getContentResolver(), perFolderMediaItemLimit));
+
+        List<SyncedFolder> syncedFolderArrayList = mSyncedFolderProvider.getSyncedFolders();
+        List<SyncedFolder> currentAccountSyncedFoldersList = new ArrayList<>();
+        Account currentAccount = AccountUtils.getCurrentOwnCloudAccount(SyncedFoldersActivity.this);
+        for (SyncedFolder syncedFolder : syncedFolderArrayList) {
+            if (syncedFolder.getAccount().equals(currentAccount.name)) {
+                currentAccountSyncedFoldersList.add(syncedFolder);
+            }
+        }
 
-                syncFolderItems = sortSyncedFolderItems(mergeFolderData(currentAccountSyncedFoldersList,
-                        mediaFolders));
+        List<SyncedFolderDisplayItem> syncFolderItems = sortSyncedFolderItems(
+                mergeFolderData(currentAccountSyncedFoldersList, mediaFolders));
 
-                mHandler.post(new TimerTask() {
-                    @Override
-                    public void run() {
-                        mAdapter.setSyncFolderItems(syncFolderItems);
-                        setListShown(true);
-                    }
-                });
-            }
-        }).start();
+        mAdapter.setSyncFolderItems(syncFolderItems);
+        mAdapter.notifyDataSetChanged();
+        setListShown(true);
     }
 
     /**
@@ -215,20 +241,23 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
         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);
-                syncedFoldersMap.remove(mediaFolder.absolutePath);
-                result.add(createSyncedFolder(syncedFolder, mediaFolder));
+            if (syncedFoldersMap.containsKey(mediaFolder.absolutePath+"-"+mediaFolder.type)) {
+                SyncedFolder syncedFolder = syncedFoldersMap.get(mediaFolder.absolutePath+"-"+mediaFolder.type);
+                syncedFoldersMap.remove(mediaFolder.absolutePath+"-"+mediaFolder.type);
+
+                if (MediaFolderType.CUSTOM == syncedFolder.getType()) {
+                    result.add(createSyncedFolderWithoutMediaFolder(syncedFolder));
+                } else {
+                    result.add(createSyncedFolder(syncedFolder, mediaFolder));
+                }
             } else {
                 result.add(createSyncedFolderFromMediaFolder(mediaFolder));
             }
         }
 
         for (SyncedFolder syncedFolder : syncedFoldersMap.values()) {
-            SyncedFolderDisplayItem syncedFolderDisplayItem = createSyncedFolderWithoutMediaFolder(syncedFolder);
-            result.add(syncedFolderDisplayItem);
+            result.add(createSyncedFolderWithoutMediaFolder(syncedFolder));
         }
 
         return result;
@@ -277,6 +306,11 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
 
     @NonNull
     private SyncedFolderDisplayItem createSyncedFolderWithoutMediaFolder(@NonNull SyncedFolder syncedFolder) {
+
+        File localFolder = new File(syncedFolder.getLocalPath());
+        File[] files = getFileList(localFolder);
+        List<String> filePaths = getDisplayFilePathList(files);
+
         return new SyncedFolderDisplayItem(
                 syncedFolder.getId(),
                 syncedFolder.getLocalPath(),
@@ -287,7 +321,10 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
                 syncedFolder.getAccount(),
                 syncedFolder.getUploadAction(),
                 syncedFolder.isEnabled(),
-                new File(syncedFolder.getLocalPath()).getName());
+                filePaths,
+                localFolder.getName(),
+                files.length,
+                syncedFolder.getType());
     }
 
     /**
@@ -311,7 +348,8 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
                 syncedFolder.isEnabled(),
                 mediaFolder.filePaths,
                 mediaFolder.folderName,
-                mediaFolder.numberOfFiles);
+                mediaFolder.numberOfFiles,
+                mediaFolder.type);
     }
 
     /**
@@ -334,7 +372,42 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
                 false,
                 mediaFolder.filePaths,
                 mediaFolder.folderName,
-                mediaFolder.numberOfFiles);
+                mediaFolder.numberOfFiles,
+                mediaFolder.type);
+    }
+
+    private File[] getFileList(File localFolder) {
+        File[] files = localFolder.listFiles(new FileFilter() {
+            @Override
+            public boolean accept(File pathname) {
+                return !pathname.isDirectory();
+            }
+        });
+
+        if (files != null) {
+            Arrays.sort(files, new Comparator<File>() {
+                public int compare(File f1, File f2) {
+                    return Long.valueOf(f1.lastModified()).compareTo(f2.lastModified());
+                }
+            });
+        } else {
+            files = new File[]{};
+        }
+
+        return files;
+    }
+
+    private List<String> getDisplayFilePathList(File[] files) {
+        List<String> filePaths = null;
+
+        if (files != null && files.length > 0) {
+            filePaths = new ArrayList<>();
+            for (int i = 0; i < 7 && i < files.length; i++) {
+                filePaths.add(files[i].getAbsolutePath());
+            }
+        }
+
+        return filePaths;
     }
 
     /**
@@ -348,7 +421,7 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
         Map<String, SyncedFolder> result = new HashMap<>();
         if (syncFolders != null) {
             for (SyncedFolder syncFolder : syncFolders) {
-                result.put(syncFolder.getLocalPath(), syncFolder);
+                result.put(syncFolder.getLocalPath()+"-"+syncFolder.getType(), syncFolder);
             }
         }
         return result;
@@ -389,6 +462,7 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
                 result = super.onOptionsItemSelected(item);
                 break;
         }
+
         return result;
     }
 
@@ -409,15 +483,27 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
 
     @Override
     public void onSyncStatusToggleClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem) {
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(MainApp.getAppContext().
+                getContentResolver());
+
         if (syncedFolderDisplayItem.getId() > UNPERSISTED_ID) {
-            mSyncedFolderProvider.updateFolderSyncEnabled(syncedFolderDisplayItem.getId(),
+            mSyncedFolderProvider.updateSyncedFolderEnabled(syncedFolderDisplayItem.getId(),
                     syncedFolderDisplayItem.isEnabled());
         } else {
-            long storedId = mSyncedFolderProvider.storeFolderSync(syncedFolderDisplayItem);
+            long storedId = mSyncedFolderProvider.storeSyncedFolder(syncedFolderDisplayItem);
             if (storedId != -1) {
                 syncedFolderDisplayItem.setId(storedId);
             }
         }
+
+        if (syncedFolderDisplayItem.isEnabled()) {
+            FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolderDisplayItem);
+        } else {
+            String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + syncedFolderDisplayItem.getId();
+            arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
+        }
+        FilesSyncHelper.scheduleNJobs(false);
+
     }
 
     @Override
@@ -437,36 +523,76 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
                 && resultCode == RESULT_OK && mSyncedFolderPreferencesDialogFragment != null) {
             OCFile chosenFolder = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER);
             mSyncedFolderPreferencesDialogFragment.setRemoteFolderSummary(chosenFolder.getRemotePath());
-
-        } else {
+        } if (requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_LOCAL_FOLDER
+                && resultCode == RESULT_OK && mSyncedFolderPreferencesDialogFragment != null) {
+            String localPath = data.getStringExtra(UploadFilesActivity.EXTRA_CHOSEN_FILES);
+            mSyncedFolderPreferencesDialogFragment.setLocalFolderSummary(localPath);
+        }
+        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
-            long storedId = mSyncedFolderProvider.storeFolderSync(item);
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(MainApp.getAppContext().
+                getContentResolver());
+
+        // custom folders newly created aren't in the list already,
+        // so triggering a refresh
+        if (MediaFolderType.CUSTOM.equals(syncedFolder.getType()) && syncedFolder.getId() == UNPERSISTED_ID) {
+            SyncedFolderDisplayItem newCustomFolder = new SyncedFolderDisplayItem(
+                    SyncedFolder.UNPERSISTED_ID, syncedFolder.getLocalPath(), syncedFolder.getRemotePath(),
+                    syncedFolder.getWifiOnly(), syncedFolder.getChargingOnly(), syncedFolder.getSubfolderByDate(),
+                    syncedFolder.getAccount(), syncedFolder.getUploadAction(), syncedFolder.getEnabled(),
+                    new File(syncedFolder.getLocalPath()).getName(), syncedFolder.getType());
+            long storedId = mSyncedFolderProvider.storeSyncedFolder(newCustomFolder);
             if (storedId != -1) {
-                item.setId(storedId);
+                newCustomFolder.setId(storedId);
+                if (newCustomFolder.isEnabled()) {
+                    FilesSyncHelper.insertAllDBEntriesForSyncedFolder(newCustomFolder);
+                } else {
+                    String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + newCustomFolder.getId();
+                    arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
+                }
+                FilesSyncHelper.scheduleNJobs(false);
             }
-
+            mAdapter.addSyncFolderItem(newCustomFolder);
         } else {
-            // existing synced folder setup to be updated
-            mSyncedFolderProvider.updateSyncFolder(item);
-        }
-        mSyncedFolderPreferencesDialogFragment = null;
+            SyncedFolderDisplayItem item = mAdapter.get(syncedFolder.getSection());
+            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
+                long storedId = mSyncedFolderProvider.storeSyncedFolder(item);
+                if (storedId != -1) {
+                    item.setId(storedId);
+                    if (item.isEnabled()) {
+                        FilesSyncHelper.insertAllDBEntriesForSyncedFolder(item);
+                    } else {
+                        String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
+                        arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
+                    }
+                    FilesSyncHelper.scheduleNJobs(false);
+                }
+            } else {
+                // existing synced folder setup to be updated
+                mSyncedFolderProvider.updateSyncFolder(item);
+                if (item.isEnabled()) {
+                    FilesSyncHelper.insertAllDBEntriesForSyncedFolder(item);
+                } else {
+                    String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
+                    arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
+                }
+                FilesSyncHelper.scheduleNJobs(false);
+            }
 
-        if (dirty) {
             mAdapter.setSyncFolderItem(syncedFolder.getSection(), item);
         }
+
+        mSyncedFolderPreferencesDialogFragment = null;
     }
 
     @Override
@@ -474,6 +600,12 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
         mSyncedFolderPreferencesDialogFragment = null;
     }
 
+    @Override
+    public void onDeleteSyncedFolderPreference(SyncedFolderParcelable syncedFolder) {
+        mSyncedFolderProvider.deleteSyncedFolder(syncedFolder.getId());
+        mAdapter.removeItem(syncedFolder.getSection());
+    }
+
     /**
      * update given synced folder with the given values.
      *
@@ -525,4 +657,13 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
                 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
         }
     }
+
+    public void onAddCustomFolderClick(View view) {
+        Log.d(TAG, "Show custom folder dialog");
+        SyncedFolderDisplayItem emptyCustomFolder = new SyncedFolderDisplayItem(
+                SyncedFolder.UNPERSISTED_ID, null, null, true, false,
+                false, AccountUtils.getCurrentOwnCloudAccount(this).name,
+                FileUploader.LOCAL_BEHAVIOUR_FORGET, false, null, MediaFolderType.CUSTOM);
+        onSyncFolderSettingsClick(0, emptyCustomFolder);
+    }
 }

+ 59 - 21
src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java

@@ -27,6 +27,8 @@ import android.graphics.PorterDuff;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Environment;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.support.v4.app.DialogFragment;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
@@ -75,6 +77,7 @@ public class UploadFilesActivity extends FileActivity implements
     private ArrayAdapter<String> mDirectories;
     private File mCurrentDir = null;
     private boolean mSelectAll = false;
+    private boolean mLocalFolderPickerMode = false;
     private LocalFileListFragment mFileListFragment;
     private Button mCancelBtn;
     protected Button mUploadBtn;
@@ -88,6 +91,10 @@ public class UploadFilesActivity extends FileActivity implements
     public static final String EXTRA_CHOSEN_FILES =
             UploadFilesActivity.class.getCanonicalName() + ".EXTRA_CHOSEN_FILES";
 
+    public static final String EXTRA_ACTION = UploadFilesActivity.class.getCanonicalName() + ".EXTRA_ACTION";
+    public final static String KEY_LOCAL_FOLDER_PICKER_MODE = UploadFilesActivity.class.getCanonicalName()
+            + ".LOCAL_FOLDER_PICKER_MODE";
+
     public static final int RESULT_OK_AND_MOVE = RESULT_FIRST_USER;
     public static final int RESULT_OK_AND_DO_NOTHING = 2;
     public static final int RESULT_OK_AND_DELETE = 3;
@@ -106,6 +113,11 @@ public class UploadFilesActivity extends FileActivity implements
         Log_OC.d(TAG, "onCreate() start");
         super.onCreate(savedInstanceState);
 
+        Bundle extras = getIntent().getExtras();
+        if (extras != null) {
+            mLocalFolderPickerMode = extras.getBoolean(KEY_LOCAL_FOLDER_PICKER_MODE, false);
+        }
+
         if(savedInstanceState != null) {
             mCurrentDir = new File(savedInstanceState.getString(UploadFilesActivity.KEY_DIRECTORY_PATH, Environment
                     .getExternalStorageDirectory().getAbsolutePath()));
@@ -130,9 +142,14 @@ public class UploadFilesActivity extends FileActivity implements
         // Inflate and set the layout view
         setContentView(R.layout.upload_files_layout);
 
+        if (mLocalFolderPickerMode) {
+            findViewById(R.id.upload_options).setVisibility(View.GONE);
+            ((AppCompatButton) findViewById(R.id.upload_files_btn_upload))
+                    .setText(R.string.uploader_btn_alternative_text);
+        }
+
         mFileListFragment = (LocalFileListFragment) getSupportFragmentManager().findFragmentById(R.id.local_files_list);
         
-        
         // Set input controllers
         mCancelBtn = (Button) findViewById(R.id.upload_files_btn_cancel);
         mCancelBtn.setOnClickListener(this);
@@ -190,10 +207,15 @@ public class UploadFilesActivity extends FileActivity implements
     public boolean onCreateOptionsMenu(Menu menu) {
         mOptionsMenu = menu;
         getMenuInflater().inflate(R.menu.upload_files_picker, menu);
-        MenuItem selectAll = menu.findItem(R.id.action_select_all);
-        setSelectAllMenuItem(selectAll, mSelectAll);
+
+        if(!mLocalFolderPickerMode) {
+            MenuItem selectAll = menu.findItem(R.id.action_select_all);
+            setSelectAllMenuItem(selectAll, mSelectAll);
+        }
+
         MenuItem switchView = menu.findItem(R.id.action_switch_view);
         switchView.setTitle(isGridView() ? R.string.action_switch_list_view : R.string.action_switch_grid_view);
+
         return super.onCreateOptionsMenu(menu);
     }
 
@@ -215,9 +237,6 @@ public class UploadFilesActivity extends FileActivity implements
                 break;
             }
             case R.id.action_sort: {
-                // Read sorting order, default to sort by name ascending
-                Integer sortOrder = PreferenceManager.getSortOrder(this);
-
                 FragmentManager fm = getSupportFragmentManager();
                 FragmentTransaction ft = fm.beginTransaction();
                 ft.addToBackStack(null);
@@ -290,7 +309,6 @@ public class UploadFilesActivity extends FileActivity implements
         }
         return true;
     }
-
     
     @Override
     public void onBackPressed() {
@@ -308,9 +326,10 @@ public class UploadFilesActivity extends FileActivity implements
         }
 
         // invalidate checked state when navigating directories
-        setSelectAllMenuItem(mOptionsMenu.findItem(R.id.action_select_all), false);
+        if(!mLocalFolderPickerMode) {
+            setSelectAllMenuItem(mOptionsMenu.findItem(R.id.action_select_all), false);
+        }
     }
-
     
     @Override
     protected void onSaveInstanceState(Bundle outState) {
@@ -361,15 +380,16 @@ public class UploadFilesActivity extends FileActivity implements
         }
     }
 
-
-    // Custom array adapter to override text colors
+    /**
+     * Custom array adapter to override text colors
+     */
     private class CustomArrayAdapter<T> extends ArrayAdapter<T> {
     
         public CustomArrayAdapter(UploadFilesActivity ctx, int view) {
             super(ctx, view);
         }
-    
-        public View getView(int position, View convertView, ViewGroup parent) {
+
+        public @NonNull View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
             View v = super.getView(position, convertView, parent);
     
             ((TextView) v).setTextColor(getResources().getColorStateList(
@@ -377,8 +397,7 @@ public class UploadFilesActivity extends FileActivity implements
             return v;
         }
     
-        public View getDropDownView(int position, View convertView,
-                ViewGroup parent) {
+        public View getDropDownView(int position, View convertView, @NonNull ViewGroup parent) {
             View v = super.getDropDownView(position, convertView, parent);
     
             ((TextView) v).setTextColor(getResources().getColorStateList(
@@ -386,7 +405,6 @@ public class UploadFilesActivity extends FileActivity implements
     
             return v;
         }
-    
     }
 
     /**
@@ -394,9 +412,11 @@ public class UploadFilesActivity extends FileActivity implements
      */
     @Override
     public void onDirectoryClick(File directory) {
-        // invalidate checked state when navigating directories
-        MenuItem selectAll = mOptionsMenu.findItem(R.id.action_select_all);
-        setSelectAllMenuItem(selectAll, false);
+        if(!mLocalFolderPickerMode) {
+            // invalidate checked state when navigating directories
+            MenuItem selectAll = mOptionsMenu.findItem(R.id.action_select_all);
+            setSelectAllMenuItem(selectAll, false);
+        }
 
         pushDirname(directory);
         ActionBar actionBar = getSupportActionBar();
@@ -419,6 +439,14 @@ public class UploadFilesActivity extends FileActivity implements
         return mCurrentDir;
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isFolderPickerMode() {
+        return mLocalFolderPickerMode;
+    }
+
     /**
      * Performs corresponding action when user presses 'Cancel' or 'Upload' button
      * 
@@ -432,7 +460,17 @@ public class UploadFilesActivity extends FileActivity implements
             finish();
 
         } else if (v.getId() == R.id.upload_files_btn_upload) {
-            new CheckAvailableSpaceTask().execute(mBehaviourSpinner.getSelectedItemPosition()==0);
+            if(mLocalFolderPickerMode) {
+                Intent data = new Intent();
+                if(mCurrentDir != null) {
+                    data.putExtra(EXTRA_CHOSEN_FILES, mCurrentDir.getAbsolutePath());
+                }
+                setResult(RESULT_OK, data);
+
+                finish();
+            } else {
+                new CheckAvailableSpaceTask().execute(mBehaviourSpinner.getSelectedItemPosition() == 0);
+            }
         }
     }
 
@@ -445,7 +483,7 @@ public class UploadFilesActivity extends FileActivity implements
     private class CheckAvailableSpaceTask extends AsyncTask<Boolean, Void, Boolean> {
 
         /**
-         * Updates the UI before trying the movement
+         * Updates the UI before trying the movement.
          */
         @Override
         protected void onPreExecute () {

+ 38 - 19
src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java

@@ -22,7 +22,6 @@
 package com.owncloud.android.ui.activity;
 
 import android.accounts.Account;
-import android.accounts.AccountManager;
 import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -30,9 +29,11 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
+import android.content.SharedPreferences;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.preference.PreferenceManager;
 import android.support.design.widget.BottomNavigationView;
 import android.support.v4.app.FragmentTransaction;
 import android.view.Menu;
@@ -41,21 +42,22 @@ import android.view.MenuItem;
 import android.view.View;
 import android.widget.Toast;
 
+import com.evernote.android.job.JobRequest;
 import com.owncloud.android.R;
-import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.db.OCUpload;
-import com.owncloud.android.db.UploadResult;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
+import com.owncloud.android.jobs.FilesSyncJob;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.operations.CheckCurrentCredentialsOperation;
 import com.owncloud.android.ui.fragment.UploadListFragment;
-import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.AnalyticsUtils;
+import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.FilesSyncHelper;
 import com.owncloud.android.utils.MimeTypeUtil;
 
 import java.io.File;
@@ -74,8 +76,12 @@ public class UploadListActivity extends FileActivity implements UploadListFragme
 
     private static final String SCREEN_NAME = "Uploads";
 
+    private static final String EXPERT_MODE = "expert_mode";
+
     private UploadMessagesReceiver mUploadMessagesReceiver;
 
+    private Menu mMenu;
+
     @Override
     public void showFiles(boolean onDeviceOnly) {
         super.showFiles(onDeviceOnly);
@@ -211,9 +217,14 @@ public class UploadListActivity extends FileActivity implements UploadListFragme
                 } else {
                     openDrawer();
                 }
+                break;
+
             case R.id.action_retry_uploads:
                 FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
                 requester.retryFailedUploads(this, null, null);
+                if (mMenu != null) {
+                    mMenu.removeItem(R.id.action_retry_uploads);
+                }
                 break;
 
             case R.id.action_clear_failed_uploads:
@@ -234,6 +245,19 @@ public class UploadListActivity extends FileActivity implements UploadListFragme
                 uploadListFragment.updateUploads();
                 break;
 
+            case R.id.action_force_rescan:
+                new JobRequest.Builder(FilesSyncJob.TAG)
+                        .setExact(1_000L)
+                        .setUpdateCurrent(false)
+                        .build()
+                        .schedule();
+                
+                if (mMenu != null) {
+                    mMenu.removeItem(R.id.action_force_rescan);
+                }
+
+                break;
+
             default:
                 retval = super.onOptionsItemSelected(item);
         }
@@ -243,8 +267,14 @@ public class UploadListActivity extends FileActivity implements UploadListFragme
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
-        MenuInflater inflater = getMenuInflater();
-        inflater.inflate(R.menu.upload_list_menu, menu);
+        SharedPreferences appPrefs =
+                PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+        if (appPrefs.getBoolean(EXPERT_MODE, false)) {
+            MenuInflater inflater = getMenuInflater();
+            inflater.inflate(R.menu.upload_list_menu, menu);
+            mMenu = menu;
+        }
+
         return true;
     }
 
@@ -252,17 +282,7 @@ public class UploadListActivity extends FileActivity implements UploadListFragme
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
         if (requestCode == FileActivity.REQUEST_CODE__UPDATE_CREDENTIALS && resultCode == RESULT_OK) {
-            // Retry uploads of the updated account
-            Account account = AccountUtils.getOwnCloudAccountByName(
-                this,
-                data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
-            );
-            FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-            requester.retryFailedUploads(
-                this,
-                account,
-                UploadResult.CREDENTIAL_ERROR
-            );
+            FilesSyncHelper.restartJobsIfNeeded();
         }
     }
 
@@ -283,8 +303,7 @@ public class UploadListActivity extends FileActivity implements UploadListFragme
 
             } else {
                 // already updated -> just retry!
-                FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-                requester.retryFailedUploads(this, account, UploadResult.CREDENTIAL_ERROR);
+                FilesSyncHelper.restartJobsIfNeeded();
             }
 
         } else {

+ 2 - 8
src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java

@@ -63,7 +63,6 @@ import com.owncloud.android.authentication.AuthenticatorActivity;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.PushConfigurationState;
 import com.owncloud.android.datamodel.SyncedFolderProvider;
-import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.lib.common.UserInfo;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
@@ -410,16 +409,11 @@ public class UserInfoActivity extends FileActivity {
                                             contentResolver);
                                     syncedFolderProvider.deleteSyncFoldersForAccount(account);
 
-                                    UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(
-                                            contentResolver, getActivity());
-                                    uploadsStorageManager.cancelPendingAutoUploadJobsForAccount(account);
-                                    uploadsStorageManager.removeAccountUploads(account);
-
                                     // disable daily backup
                                     ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(
                                             contentResolver);
 
-                                    arbitraryDataProvider.storeOrUpdateKeyValue(account,
+                                    arbitraryDataProvider.storeOrUpdateKeyValue(account.name,
                                             ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP,
                                             "false");
 
@@ -433,7 +427,7 @@ public class UserInfoActivity extends FileActivity {
                                         PushConfigurationState pushArbitraryData = gson.fromJson(arbitraryDataPushString,
                                                 PushConfigurationState.class);
                                         pushArbitraryData.setShouldBeDeleted(true);
-                                        arbitraryDataProvider.storeOrUpdateKeyValue(account, PushUtils.KEY_PUSH,
+                                        arbitraryDataProvider.storeOrUpdateKeyValue(account.name, PushUtils.KEY_PUSH,
                                                 gson.toJson(pushArbitraryData));
                                         EventBus.getDefault().post(new TokenPushEvent());
                                     }

+ 51 - 83
src/main/java/com/owncloud/android/ui/adapter/ExpandableUploadListAdapter.java

@@ -1,21 +1,21 @@
 /**
- *  ownCloud Android client application
+ * ownCloud Android client application
  *
- *  @author LukeOwncloud
- *  @author masensio
- *  Copyright (C) 2016 ownCloud Inc.
+ * @author LukeOwncloud
+ * @author masensio
+ * Copyright (C) 2016 ownCloud Inc.
  *
- *  This program is free software: you can redistribute it and/or modify
- *  it under the terms of the GNU General Public License version 2,
- *  as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
  *
- *  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 General Public License for more details.
+ * 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 General Public License for more details.
  *
- *  You should have received a copy of the GNU General Public License
- *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 package com.owncloud.android.ui.adapter;
 
@@ -26,7 +26,6 @@ import android.graphics.Bitmap;
 import android.text.format.DateUtils;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 import android.widget.BaseExpandableListAdapter;
 import android.widget.ExpandableListView;
@@ -34,7 +33,6 @@ import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
-import android.widget.Toast;
 
 import com.owncloud.android.R;
 import com.owncloud.android.authentication.AccountUtils;
@@ -43,7 +41,6 @@ import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus;
 import com.owncloud.android.db.OCUpload;
-import com.owncloud.android.db.UploadResult;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -102,10 +99,10 @@ public class ExpandableUploadListAdapter extends BaseExpandableListAdapter imple
 
             @Override
             public int compare(OCUpload upload1, OCUpload upload2) {
-                if (upload1 == null){
+                if (upload1 == null) {
                     return -1;
                 }
-                if (upload2 == null){
+                if (upload2 == null) {
                     return 1;
                 }
                 if (UploadStatus.UPLOAD_IN_PROGRESS.equals(upload1.getUploadStatus())) {
@@ -153,7 +150,7 @@ public class ExpandableUploadListAdapter extends BaseExpandableListAdapter imple
         mUploadGroups[0] = new UploadGroup(mParentActivity.getString(R.string.uploads_view_group_current_uploads)) {
             @Override
             public void refresh() {
-                items = mUploadsStorageManager.getCurrentAndPendingUploads();
+                items = mUploadsStorageManager.getCurrentAndPendingUploadsForCurrentAccount();
                 Arrays.sort(items, comparator);
             }
 
@@ -165,7 +162,7 @@ public class ExpandableUploadListAdapter extends BaseExpandableListAdapter imple
         mUploadGroups[1] = new UploadGroup(mParentActivity.getString(R.string.uploads_view_group_failed_uploads)) {
             @Override
             public void refresh() {
-                items = mUploadsStorageManager.getFailedButNotDelayedUploads();
+                items = mUploadsStorageManager.getFailedButNotDelayedUploadsForCurrentAccount();
                 Arrays.sort(items, comparator);
             }
 
@@ -178,7 +175,7 @@ public class ExpandableUploadListAdapter extends BaseExpandableListAdapter imple
         mUploadGroups[2] = new UploadGroup(mParentActivity.getString(R.string.uploads_view_group_finished_uploads)) {
             @Override
             public void refresh() {
-                items = mUploadsStorageManager.getFinishedUploads();
+                items = mUploadsStorageManager.getFinishedUploadsForCurrentAccount();
                 Arrays.sort(items, comparator);
             }
 
@@ -292,33 +289,33 @@ public class ExpandableUploadListAdapter extends BaseExpandableListAdapter imple
                             /// ... unbind the old progress bar, if any; ...
                             if (mProgressListener != null) {
                                 binder.removeDatatransferProgressListener(
-                                    mProgressListener,
-                                    mProgressListener.getUpload()   // the one that was added
+                                        mProgressListener,
+                                        mProgressListener.getUpload()   // the one that was added
                                 );
                             }
                             /// ... then, bind the current progress bar to listen for updates
                             mProgressListener = new ProgressListener(upload, progressBar);
                             binder.addDatatransferProgressListener(
-                                mProgressListener,
-                                upload
+                                    mProgressListener,
+                                    upload
                             );
 
                         } else {
                             /// not really uploading; stop listening progress if view is reused!
                             if (convertView != null &&
                                     mProgressListener != null &&
-                                    mProgressListener.isWrapping(progressBar))  {
+                                    mProgressListener.isWrapping(progressBar)) {
                                 binder.removeDatatransferProgressListener(
-                                    mProgressListener,
-                                    mProgressListener.getUpload()   // the one that was added
+                                        mProgressListener,
+                                        mProgressListener.getUpload()   // the one that was added
                                 );
                                 mProgressListener = null;
                             }
                         }
                     } else {
                         Log_OC.w(
-                            TAG,
-                            "FileUploaderBinder not ready yet for upload " + upload.getRemotePath()
+                                TAG,
+                                "FileUploaderBinder not ready yet for upload " + upload.getRemotePath()
                         );
                     }
                     uploadDateTextView.setVisibility(View.GONE);
@@ -336,13 +333,14 @@ public class ExpandableUploadListAdapter extends BaseExpandableListAdapter imple
             }
             statusTextView.setText(status);
 
+            /// bind listeners to perform actions
             /// bind listeners to perform actions
             ImageButton rightButton = (ImageButton) view.findViewById(R.id.upload_right_button);
             if (upload.getUploadStatus() == UploadStatus.UPLOAD_IN_PROGRESS) {
                 //Cancel
                 rightButton.setImageResource(R.drawable.ic_action_cancel_grey);
                 rightButton.setVisibility(View.VISIBLE);
-                rightButton.setOnClickListener(new OnClickListener() {
+                rightButton.setOnClickListener(new View.OnClickListener() {
                     @Override
                     public void onClick(View v) {
                         FileUploader.FileUploaderBinder uploaderBinder = mParentActivity.getFileUploaderBinder();
@@ -357,7 +355,7 @@ public class ExpandableUploadListAdapter extends BaseExpandableListAdapter imple
                 //Delete
                 rightButton.setImageResource(R.drawable.ic_action_delete_grey);
                 rightButton.setVisibility(View.VISIBLE);
-                rightButton.setOnClickListener(new OnClickListener() {
+                rightButton.setOnClickListener(new View.OnClickListener() {
                     @Override
                     public void onClick(View v) {
                         mUploadsStorageManager.removeUpload(upload);
@@ -368,41 +366,8 @@ public class ExpandableUploadListAdapter extends BaseExpandableListAdapter imple
             } else {    // UploadStatus.UPLOAD_SUCCESS
                 rightButton.setVisibility(View.INVISIBLE);
             }
-
-            // retry
-            if (upload.getUploadStatus() == UploadStatus.UPLOAD_FAILED) {
-                if (UploadResult.CREDENTIAL_ERROR.equals(upload.getLastResult())) {
-                    view.setOnClickListener(new OnClickListener() {
-                        @Override
-                        public void onClick(View v) {
-                            mParentActivity.getFileOperationsHelper().checkCurrentCredentials(
-                                upload.getAccount(mParentActivity)
-                            );
-                        }
-                    });
-
-                } else {
-                    // not a credentials error
-                    view.setOnClickListener(new OnClickListener() {
-                        @Override
-                        public void onClick(View v) {
-                        File file = new File(upload.getLocalPath());
-                        if (file.exists()) {
-                            FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-                            requester.retry(mParentActivity, upload);
-                            refreshView();
-                        } else {
-                            final String message = String.format(
-                                mParentActivity.getString(R.string.local_file_not_found_toast)
-                            );
-                            Toast.makeText(mParentActivity, message, Toast.LENGTH_SHORT).show();
-                        }
-                        }
-                    });
-                }
-            } else {
-                view.setOnClickListener(null);
-            }
+            
+            view.setOnClickListener(null);
 
             /// Set icon or thumbnail
             ImageView fileIcon = (ImageView) view.findViewById(R.id.thumbnail);
@@ -509,7 +474,7 @@ public class ExpandableUploadListAdapter extends BaseExpandableListAdapter imple
      * the given upload.
      *
      * @param upload        Upload to describe.
-     * @return              Text describing the status of the given upload.
+     * @return Text describing the status of the given upload.
      */
     private String getStatusText(OCUpload upload) {
 
@@ -533,37 +498,37 @@ public class ExpandableUploadListAdapter extends BaseExpandableListAdapter imple
                 switch (upload.getLastResult()) {
                     case CREDENTIAL_ERROR:
                         status = mParentActivity.getString(
-                            R.string.uploads_view_upload_status_failed_credentials_error
+                                R.string.uploads_view_upload_status_failed_credentials_error
                         );
                         break;
                     case FOLDER_ERROR:
                         status = mParentActivity.getString(
-                            R.string.uploads_view_upload_status_failed_folder_error
+                                R.string.uploads_view_upload_status_failed_folder_error
                         );
                         break;
                     case FILE_NOT_FOUND:
                         status = mParentActivity.getString(
-                            R.string.uploads_view_upload_status_failed_localfile_error
+                                R.string.uploads_view_upload_status_failed_localfile_error
                         );
                         break;
                     case FILE_ERROR:
                         status = mParentActivity.getString(
-                            R.string.uploads_view_upload_status_failed_file_error
+                                R.string.uploads_view_upload_status_failed_file_error
                         );
                         break;
                     case PRIVILEDGES_ERROR:
                         status = mParentActivity.getString(
-                            R.string.uploads_view_upload_status_failed_permission_error
+                                R.string.uploads_view_upload_status_failed_permission_error
                         );
                         break;
                     case NETWORK_CONNECTION:
                         status = mParentActivity.getString(
-                            R.string.uploads_view_upload_status_failed_connection_error
+                                R.string.uploads_view_upload_status_failed_connection_error
                         );
                         break;
                     case DELAYED_FOR_WIFI:
                         status = mParentActivity.getString(
-                            R.string.uploads_view_upload_status_waiting_for_wifi
+                                R.string.uploads_view_upload_status_waiting_for_wifi
                         );
                         break;
                     case DELAYED_FOR_CHARGING:
@@ -572,32 +537,35 @@ public class ExpandableUploadListAdapter extends BaseExpandableListAdapter imple
                         break;
                     case CONFLICT_ERROR:
                         status = mParentActivity.getString(
-                            R.string.uploads_view_upload_status_conflict
+                                R.string.uploads_view_upload_status_conflict
                         );
                         break;
                     case SERVICE_INTERRUPTED:
-                        status =  mParentActivity.getString(
-                            R.string.uploads_view_upload_status_service_interrupted
+                        status = mParentActivity.getString(
+                                R.string.uploads_view_upload_status_service_interrupted
                         );
                         break;
                     case UNKNOWN:
                         status = mParentActivity.getString(
-                            R.string.uploads_view_upload_status_unknown_fail
+                                R.string.uploads_view_upload_status_unknown_fail
                         );
                         break;
                     case CANCELLED:
                         // should not get here ; cancelled uploads should be wiped out
                         status = mParentActivity.getString(
-                            R.string.uploads_view_upload_status_cancelled
+                                R.string.uploads_view_upload_status_cancelled
                         );
                         break;
                     case UPLOADED:
                         // should not get here ; status should be UPLOAD_SUCCESS
-                        status =  mParentActivity.getString(R.string.uploads_view_upload_status_succeeded);
+                        status = mParentActivity.getString(R.string.uploads_view_upload_status_succeeded);
                         break;
                     case MAINTENANCE_MODE:
                         status = mParentActivity.getString(R.string.maintenance_mode);
                         break;
+                    case LOCK_FAILED:
+                        status = mParentActivity.getString(R.string.lock_failed);
+                        break;
                     default:
                         status = "Naughty devs added a new fail result but no description for the user";
                         break;
@@ -747,8 +715,8 @@ public class ExpandableUploadListAdapter extends BaseExpandableListAdapter imple
         public boolean isWrapping(ProgressBar progressBar) {
             ProgressBar wrappedProgressBar = mProgressBar.get();
             return (
-                wrappedProgressBar != null &&
-                wrappedProgressBar == progressBar   // on purpose; don't replace with equals
+                    wrappedProgressBar != null &&
+                            wrappedProgressBar == progressBar   // on purpose; don't replace with equals
             );
         }
 

+ 19 - 4
src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java

@@ -41,6 +41,7 @@ import com.owncloud.android.utils.FileStorageUtils;
 import com.owncloud.android.utils.MimeTypeUtil;
 
 import java.io.File;
+import java.io.FileFilter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -58,10 +59,12 @@ public class LocalFileListAdapter extends BaseAdapter implements FilterableListA
 
     private Context mContext;
     private File[] mFiles = null;
-    private Vector<File> mFilesAll = new Vector<File>();
+    private Vector<File> mFilesAll = new Vector<>();
+    private boolean mLocalFolderPicker;
 
-    public LocalFileListAdapter(File directory, Context context) {
+    public LocalFileListAdapter(boolean localFolderPickerMode, File directory, Context context) {
         mContext = context;
+        mLocalFolderPicker = localFolderPickerMode;
 
         // Read sorting order, default to sort by name ascending
         FileStorageUtils.mSortOrder = PreferenceManager.getSortOrder(context);
@@ -272,7 +275,11 @@ public class LocalFileListAdapter extends BaseAdapter implements FilterableListA
      * @param directory     New file to adapt. Can be NULL, meaning "no content to adapt".
      */
     public void swapDirectory(final File directory) {
-        mFiles = (directory != null ? directory.listFiles() : null);
+        if(mLocalFolderPicker) {
+            mFiles = (directory != null ? getFolders(directory) : null);
+        } else {
+            mFiles = (directory != null ? directory.listFiles() : null);
+        }
         if (mFiles != null) {
             Arrays.sort(mFiles, new Comparator<File>() {
                 @Override
@@ -288,7 +295,6 @@ public class LocalFileListAdapter extends BaseAdapter implements FilterableListA
                 private int compareNames(File lhs, File rhs) {
                     return lhs.getName().toLowerCase().compareTo(rhs.getName().toLowerCase());                
                 }
-            
             });
 
             mFiles = FileStorageUtils.sortLocalFolder(mFiles);
@@ -317,6 +323,15 @@ public class LocalFileListAdapter extends BaseAdapter implements FilterableListA
         notifyDataSetChanged();
     }
 
+    private File[] getFolders(final File directory) {
+        return directory.listFiles(new FileFilter() {
+            @Override
+            public boolean accept(File file) {
+                return file.isDirectory();
+            }
+        });
+    }
+
     public void filter(String text){
         if(text.isEmpty()){
             mFiles = mFilesAll.toArray(new File[1]);

+ 67 - 40
src/main/java/com/owncloud/android/ui/adapter/FolderSyncAdapter.java → src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.java

@@ -1,22 +1,22 @@
 /**
  *   Nextcloud Android client application
  *
- *   @author Andy Scherzinger
- *   Copyright (C) 2016 Andy Scherzinger
- *   Copyright (C) 2016 Nextcloud
+ * @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 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.
+ * 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/>.
+ * 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;
@@ -29,10 +29,12 @@ import android.view.ViewGroup;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter;
 import com.owncloud.android.R;
+import com.owncloud.android.datamodel.MediaFolderType;
 import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.utils.ThemeUtils;
@@ -44,7 +46,7 @@ import java.util.List;
 /**
  * Adapter to display all auto-synced folders and/or instant upload media folders.
  */
-public class FolderSyncAdapter extends SectionedRecyclerViewAdapter<FolderSyncAdapter.MainViewHolder> {
+public class SyncedFolderAdapter extends SectionedRecyclerViewAdapter<SyncedFolderAdapter.MainViewHolder> {
 
     private final Context mContext;
     private final int mGridWidth;
@@ -53,19 +55,20 @@ public class FolderSyncAdapter extends SectionedRecyclerViewAdapter<FolderSyncAd
     private final List<SyncedFolderDisplayItem> mSyncFolderItems;
     private final boolean mLight;
 
-    public FolderSyncAdapter(Context context, int gridWidth, ClickListener listener, boolean light) {
+    public SyncedFolderAdapter(Context context, int gridWidth, ClickListener listener, boolean light) {
         mContext = context;
         mGridWidth = gridWidth;
         mGridTotal = gridWidth * 2;
         mListener = listener;
         mSyncFolderItems = new ArrayList<>();
         mLight = light;
+
+        shouldShowHeadersForEmptySections(true);
     }
 
     public void setSyncFolderItems(List<SyncedFolderDisplayItem> syncFolderItems) {
         mSyncFolderItems.clear();
         mSyncFolderItems.addAll(syncFolderItems);
-        notifyDataSetChanged();
     }
 
     public void setSyncFolderItem(int location, SyncedFolderDisplayItem syncFolderItem) {
@@ -73,6 +76,16 @@ public class FolderSyncAdapter extends SectionedRecyclerViewAdapter<FolderSyncAd
         notifyDataSetChanged();
     }
 
+    public void addSyncFolderItem(SyncedFolderDisplayItem syncFolderItem) {
+        mSyncFolderItems.add(syncFolderItem);
+        notifyDataSetChanged();
+    }
+
+    public void removeItem(int section) {
+        mSyncFolderItems.remove(section);
+        notifyDataSetChanged();
+    }
+
     @Override
     public int getSectionCount() {
         return mSyncFolderItems.size();
@@ -87,18 +100,39 @@ public class FolderSyncAdapter extends SectionedRecyclerViewAdapter<FolderSyncAd
         }
     }
 
+    public SyncedFolderDisplayItem get(int section) {
+        return mSyncFolderItems.get(section);
+    }
+
     @Override
     public void onBindHeaderViewHolder(final MainViewHolder holder, final int section) {
+        holder.mainHeaderContainer.setVisibility(View.VISIBLE);
+
         holder.title.setText(mSyncFolderItems.get(section).getFolderName());
+
+        if (MediaFolderType.VIDEO == mSyncFolderItems.get(section).getType()) {
+            holder.type.setImageResource(R.drawable.ic_video_18dp);
+        } else if (MediaFolderType.IMAGE == mSyncFolderItems.get(section).getType()) {
+            holder.type.setImageResource(R.drawable.ic_image_18dp);
+        } else {
+            holder.type.setImageResource(R.drawable.ic_folder_star_18dp);
+        }
+
         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));
-            }
+        holder.syncStatusButton.setOnClickListener(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.syncStatusButton.setVisibility(View.VISIBLE);
+        holder.syncStatusButton.setTag(section);
+        holder.syncStatusButton.setOnClickListener(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());
 
@@ -107,18 +141,14 @@ public class FolderSyncAdapter extends SectionedRecyclerViewAdapter<FolderSyncAd
         } else {
             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));
-                }
-            });
+            holder.menuButton.setOnClickListener(v -> mListener.onSyncFolderSettingsClick(section,
+                    mSyncFolderItems.get(section)));
         }
     }
 
+
     @Override
     public void onBindViewHolder(MainViewHolder holder, int section, int relativePosition, int absolutePosition) {
-
         if (mSyncFolderItems.get(section).getFilePaths() != null) {
             File file = new File(mSyncFolderItems.get(section).getFilePaths().get(relativePosition));
 
@@ -148,14 +178,6 @@ public class FolderSyncAdapter extends SectionedRecyclerViewAdapter<FolderSyncAd
                 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);
-        } else {
-            holder.itemView.setTag(relativePosition % mGridWidth);
-            holder.counterValue.setText(Long.toString(0));
-            holder.counterBar.setVisibility(View.VISIBLE);
-            holder.thumbnailDarkener.setVisibility(View.VISIBLE);
         }
     }
 
@@ -163,7 +185,7 @@ public class FolderSyncAdapter extends SectionedRecyclerViewAdapter<FolderSyncAd
     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);
+                        R.layout.synced_folders_item_header : R.layout.grid_sync_item, parent, false);
         return new MainViewHolder(v);
     }
 
@@ -175,16 +197,21 @@ public class FolderSyncAdapter extends SectionedRecyclerViewAdapter<FolderSyncAd
     static class MainViewHolder extends RecyclerView.ViewHolder {
         private final ImageView image;
         private final TextView title;
+        private final ImageView type;
         private final ImageButton menuButton;
         private final ImageButton syncStatusButton;
         private final LinearLayout counterBar;
         private final TextView counterValue;
         private final ImageView thumbnailDarkener;
 
+        private final RelativeLayout mainHeaderContainer;
+
         private MainViewHolder(View itemView) {
             super(itemView);
+            mainHeaderContainer = (RelativeLayout) itemView.findViewById(R.id.header_container);
             image = (ImageView) itemView.findViewById(R.id.thumbnail);
             title = (TextView) itemView.findViewById(R.id.title);
+            type = (ImageView) itemView.findViewById(R.id.type);
             menuButton = (ImageButton) itemView.findViewById(R.id.settingsButton);
             syncStatusButton = (ImageButton) itemView.findViewById(R.id.syncStatusButton);
             counterBar = (LinearLayout) itemView.findViewById(R.id.counterLayout);
@@ -194,7 +221,7 @@ public class FolderSyncAdapter extends SectionedRecyclerViewAdapter<FolderSyncAd
     }
 
     private void setSyncButtonActiveIcon(ImageButton syncStatusButton, boolean enabled) {
-        if(enabled) {
+        if (enabled) {
             syncStatusButton.setImageDrawable(ThemeUtils.tintDrawable(R.drawable.ic_cloud_sync_on,
                     ThemeUtils.primaryColor()));
         } else {

+ 3 - 1
src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java

@@ -228,7 +228,9 @@ public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, Result
             behaviour,
             mimeType,
             false,      // do not create parent folder if not existent
-            UploadFileOperation.CREATED_BY_USER
+            UploadFileOperation.CREATED_BY_USER,
+            false,
+             false
         );
     }
 

+ 4 - 2
src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.java

@@ -47,8 +47,10 @@ public class LoadingDialog extends DialogFragment {
         setCancelable(false);
     }
 
-    public LoadingDialog(String message) {
-        this.mMessage = message;
+    public static LoadingDialog newInstance(String message) {
+        LoadingDialog loadingDialog = new LoadingDialog();
+        loadingDialog.mMessage = message;
+        return loadingDialog;
     }
 
     @Override

+ 2 - 4
src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.java

@@ -105,7 +105,7 @@ public class SortingOrderDialogFragment extends DialogFragment {
         mView = inflater.inflate(R.layout.sorting_order_fragment, container, false);
 
         setupDialogElements(mView);
-        setupListeners(mView);
+        setupListeners();
 
         return mView;
     }
@@ -208,10 +208,8 @@ public class SortingOrderDialogFragment extends DialogFragment {
 
     /**
      * setup all listeners.
-     *
-     * @param view the parent view
      */
-    private void setupListeners(View view) {
+    private void setupListeners() {
         mCancel.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View view) {

+ 125 - 25
src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java

@@ -25,6 +25,7 @@ import android.app.Dialog;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.graphics.Typeface;
+import android.os.Build;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
@@ -41,13 +42,19 @@ import android.view.ViewGroup;
 import android.widget.TextView;
 
 import com.owncloud.android.R;
+import com.owncloud.android.datamodel.MediaFolderType;
 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.activity.UploadFilesActivity;
 import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.ThemeUtils;
 
+import java.io.File;
+
+import static com.owncloud.android.datamodel.SyncedFolderDisplayItem.UNPERSISTED_ID;
+
 /**
  * Dialog to show the preferences/configuration of a synced folder allowing the user to change the different parameters.
  */
@@ -57,6 +64,7 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
     public static final String SYNCED_FOLDER_PARCELABLE = "SyncedFolderParcelable";
     private static final String BEHAVIOUR_DIALOG_STATE = "BEHAVIOUR_DIALOG_STATE";
     public static final int REQUEST_CODE__SELECT_REMOTE_FOLDER = 0;
+    public static final int REQUEST_CODE__SELECT_LOCAL_FOLDER = 1;
 
     private CharSequence[] mUploadBehaviorItemStrings;
 
@@ -67,6 +75,7 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
     private AppCompatCheckBox mUploadUseSubfoldersCheckbox;
     private TextView mUploadBehaviorSummary;
     private TextView mLocalFolderPath;
+    private TextView mLocalFolderSummary;
     private TextView mRemoteFolderSummary;
 
     private SyncedFolderParcelable mSyncedFolder;
@@ -85,7 +94,7 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
         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);
+        dialogFragment.setStyle(STYLE_NORMAL, R.style.Theme_ownCloud_Dialog);
 
         return dialogFragment;
     }
@@ -116,7 +125,7 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
     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);
+        mView = inflater.inflate(R.layout.synced_folders_settings_layout, container, false);
 
         setupDialogElements(mView);
         setupListeners(mView);
@@ -132,20 +141,49 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
     private void setupDialogElements(View view) {
         int accentColor = ThemeUtils.primaryAccentColor();
 
+        if (mSyncedFolder.getType().getId() > MediaFolderType.CUSTOM.getId()) {
+            // hide local folder chooser and delete for non-custom folders
+            view.findViewById(R.id.local_folder_container).setVisibility(View.GONE);
+            view.findViewById(R.id.delete).setVisibility(View.GONE);
+        } else if (mSyncedFolder.getId() <= UNPERSISTED_ID) {
+            // Hide delete/enabled for unpersisted custom folders
+            view.findViewById(R.id.delete).setVisibility(View.GONE);
+            view.findViewById(R.id.sync_enabled).setVisibility(View.GONE);
+
+            // auto set custom folder to enabled
+            mSyncedFolder.setEnabled(true);
+
+            // switch text to create headline
+            ((TextView) view.findViewById(R.id.synced_folders_settings_title))
+                    .setText(R.string.autoupload_create_new_custom_folder);
+
+            // disable save button
+            view.findViewById(R.id.save).setEnabled(false);
+        } else {
+            view.findViewById(R.id.local_folder_container).setVisibility(View.GONE);
+        }
+
         // find/saves UI elements
         mEnabledSwitch = (SwitchCompat) view.findViewById(R.id.sync_enabled);
         ThemeUtils.tintSwitch(mEnabledSwitch, accentColor);
 
-        mLocalFolderPath = (TextView) view.findViewById(R.id.folder_sync_settings_local_folder_path);
+        mLocalFolderPath = (TextView) view.findViewById(R.id.synced_folders_settings_local_folder_path);
 
+        mLocalFolderSummary = (TextView) view.findViewById(R.id.local_folder_summary);
         mRemoteFolderSummary = (TextView) view.findViewById(R.id.remote_folder_summary);
 
         mUploadOnWifiCheckbox = (AppCompatCheckBox) view.findViewById(R.id.setting_instant_upload_on_wifi_checkbox);
         ThemeUtils.tintCheckbox(mUploadOnWifiCheckbox, accentColor);
 
-        mUploadOnChargingCheckbox = (AppCompatCheckBox) view.findViewById(
-                R.id.setting_instant_upload_on_charging_checkbox);
-        ThemeUtils.tintCheckbox(mUploadOnChargingCheckbox, accentColor);
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            view.findViewById(R.id.setting_instant_upload_on_charging_container).setVisibility(View.GONE);
+        } else {
+            view.findViewById(R.id.setting_instant_upload_on_charging_container).setVisibility(View.VISIBLE);
+
+            mUploadOnChargingCheckbox = (AppCompatCheckBox) view.findViewById(
+                    R.id.setting_instant_upload_on_charging_checkbox);
+            ThemeUtils.tintCheckbox(mUploadOnChargingCheckbox, accentColor);
+        }
 
         mUploadUseSubfoldersCheckbox = (AppCompatCheckBox) view.findViewById(
                 R.id.setting_instant_upload_path_use_subfolders_checkbox);
@@ -161,18 +199,30 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
 
         // 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());
+        if (mSyncedFolder.getLocalPath() != null && mSyncedFolder.getLocalPath().length() > 0) {
+            mLocalFolderPath.setText(
+                    DisplayUtils.createTextWithSpan(
+                            String.format(
+                                    getString(R.string.synced_folders_preferences_folder_path),
+                                    mSyncedFolder.getLocalPath()),
+                            mSyncedFolder.getFolderName(),
+                            new StyleSpan(Typeface.BOLD)));
+            mLocalFolderSummary.setText(mSyncedFolder.getLocalPath());
+        } else {
+            mLocalFolderSummary.setText(R.string.choose_local_folder);
+        }
+
+        if (mSyncedFolder.getLocalPath() != null && mSyncedFolder.getLocalPath().length() > 0) {
+            mRemoteFolderSummary.setText(mSyncedFolder.getRemotePath());
+        } else {
+            mRemoteFolderSummary.setText(R.string.choose_remote_folder);
+        }
 
         mUploadOnWifiCheckbox.setChecked(mSyncedFolder.getWifiOnly());
-        mUploadOnChargingCheckbox.setChecked(mSyncedFolder.getChargingOnly());
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            mUploadOnChargingCheckbox.setChecked(mSyncedFolder.getChargingOnly());
+        }
         mUploadUseSubfoldersCheckbox.setChecked(mSyncedFolder.getSubfolderByDate());
 
         mUploadBehaviorSummary.setText(mUploadBehaviorItemStrings[mSyncedFolder.getUploadActionInteger()]);
@@ -198,6 +248,35 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
     public void setRemoteFolderSummary(String path) {
         mSyncedFolder.setRemotePath(path);
         mRemoteFolderSummary.setText(path);
+        checkAndUpdateSaveButtonState();
+    }
+
+    /**
+     * set (new) local 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 setLocalFolderSummary(String path) {
+        mSyncedFolder.setLocalPath(path);
+        mLocalFolderSummary.setText(path);
+        mLocalFolderPath.setText(
+                DisplayUtils.createTextWithSpan(
+                        String.format(
+                                getString(R.string.synced_folders_preferences_folder_path),
+                                mSyncedFolder.getLocalPath()),
+                        new File(mSyncedFolder.getLocalPath()).getName(),
+                        new StyleSpan(Typeface.BOLD)));
+        checkAndUpdateSaveButtonState();
+    }
+
+    private void checkAndUpdateSaveButtonState() {
+        if (mSyncedFolder.getLocalPath() != null && mSyncedFolder.getRemotePath() != null) {
+            mView.findViewById(R.id.save).setEnabled(true);
+        } else {
+            mView.findViewById(R.id.save).setEnabled(false);
+        }
     }
 
     /**
@@ -208,6 +287,7 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
     private void setupListeners(View view) {
         mSave.setOnClickListener(new OnSyncedFolderSaveClickListener());
         mCancel.setOnClickListener(new OnSyncedFolderCancelClickListener());
+        view.findViewById(R.id.delete).setOnClickListener(new OnSyncedFolderDeleteClickListener());
 
         view.findViewById(R.id.setting_instant_upload_on_wifi_container).setOnClickListener(
                 new OnClickListener() {
@@ -218,14 +298,17 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
                     }
                 });
 
-        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();
-                    }
-                });
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+
+            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() {
@@ -240,12 +323,19 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
             @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.local_folder_container).setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Intent action = new Intent(getActivity(), UploadFilesActivity.class);
+                action.putExtra(UploadFilesActivity.KEY_LOCAL_FOLDER_PICKER_MODE, true);
+                getActivity().startActivityForResult(action, REQUEST_CODE__SELECT_LOCAL_FOLDER);
+            }
+        });
+
         view.findViewById(R.id.sync_enabled).setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
@@ -330,10 +420,20 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
         }
     }
 
+    private class OnSyncedFolderDeleteClickListener implements OnClickListener {
+        @Override
+        public void onClick(View v) {
+            dismiss();
+            ((OnSyncedFolderPreferenceListener) getActivity()).onDeleteSyncedFolderPreference(mSyncedFolder);
+        }
+    }
+
     public interface OnSyncedFolderPreferenceListener {
         void onSaveSyncedFolderPreference(SyncedFolderParcelable syncedFolder);
 
         void onCancelSyncedFolderPreference();
+
+        void onDeleteSyncedFolderPreference(SyncedFolderParcelable syncedFolder);
     }
 
     @Override

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

@@ -23,6 +23,7 @@ package com.owncloud.android.ui.dialog.parcel;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.owncloud.android.datamodel.MediaFolderType;
 import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
 import com.owncloud.android.files.services.FileUploader;
 
@@ -38,6 +39,7 @@ public class SyncedFolderParcelable implements Parcelable {
     private Boolean mEnabled = false;
     private Boolean mSubfolderByDate = false;
     private Integer mUploadAction;
+    private MediaFolderType mType;
     private long mId;
     private String mAccount;
     private int mSection;
@@ -54,6 +56,7 @@ public class SyncedFolderParcelable implements Parcelable {
         mChargingOnly = syncedFolderDisplayItem.getChargingOnly();
         mEnabled = syncedFolderDisplayItem.isEnabled();
         mSubfolderByDate = syncedFolderDisplayItem.getSubfolderByDate();
+        mType = syncedFolderDisplayItem.getType();
         mAccount = syncedFolderDisplayItem.getAccount();
         mUploadAction = syncedFolderDisplayItem.getUploadAction();
         mSection = section;
@@ -68,6 +71,7 @@ public class SyncedFolderParcelable implements Parcelable {
         mChargingOnly = read.readInt() != 0;
         mEnabled = read.readInt() != 0;
         mSubfolderByDate = read.readInt() != 0;
+        mType = MediaFolderType.getById(read.readInt());
         mAccount = read.readString();
         mUploadAction = read.readInt();
         mSection = read.readInt();
@@ -83,6 +87,7 @@ public class SyncedFolderParcelable implements Parcelable {
         dest.writeInt(mChargingOnly ? 1 : 0);
         dest.writeInt(mEnabled ? 1 : 0);
         dest.writeInt(mSubfolderByDate ? 1 : 0);
+        dest.writeInt(mType.getId());
         dest.writeString(mAccount);
         dest.writeInt(mUploadAction);
         dest.writeInt(mSection);
@@ -163,6 +168,14 @@ public class SyncedFolderParcelable implements Parcelable {
         this.mSubfolderByDate = mSubfolderByDate;
     }
 
+    public MediaFolderType getType() {
+        return mType;
+    }
+
+    public void setType(MediaFolderType mType) {
+        this.mType = mType;
+    }
+
     public Integer getUploadAction() {
         return mUploadAction;
     }

+ 10 - 15
src/main/java/com/owncloud/android/services/ShutdownReceiver.java → src/main/java/com/owncloud/android/ui/events/InitiateSyncedFolder.java

@@ -3,7 +3,6 @@
  *
  * @author Mario Danic
  * Copyright (C) 2017 Mario Danic
- * Copyright (C) 2017 Nextcloud GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -18,23 +17,19 @@
  * 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;
+package com.owncloud.android.ui.events;
 
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
+import com.owncloud.android.datamodel.SyncedFolder;
 
-import com.owncloud.android.MainApp;
+public class InitiateSyncedFolder {
+    private final SyncedFolder syncedFolder;
 
-/**
- * Handles shutdown procedure - basically just waits a little bit for all jobs to finish
- */
 
-public class ShutdownReceiver extends BroadcastReceiver {
-    @Override
-    public void onReceive(final Context context, final Intent intent) {
-        if (MainApp.getSyncedFolderObserverService() != null) {
-            MainApp.getSyncedFolderObserverService().onDestroy();
-        }
+    public InitiateSyncedFolder(SyncedFolder syncedFolder) {
+        this.syncedFolder = syncedFolder;
+    }
+
+    public SyncedFolder getSyncedFolder() {
+        return syncedFolder;
     }
 }

+ 1 - 1
src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java

@@ -955,7 +955,7 @@ public class ExtendedListFragment extends Fragment
             maxColumnSize = maxColumnSizePortrait;
         }
 
-        if (mGridView.getNumColumns() > maxColumnSize) {
+        if (mGridView != null && mGridView.getNumColumns() > maxColumnSize) {
             mGridView.setNumColumns(maxColumnSize);
             mGridView.invalidateViews();
         }

+ 40 - 7
src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java

@@ -26,6 +26,8 @@ import android.os.Bundle;
 import android.os.Environment;
 import android.util.SparseBooleanArray;
 import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AbsListView;
@@ -89,7 +91,6 @@ public class LocalFileListFragment extends ExtendedListFragment {
         }
     }
     
-    
     /**
      * {@inheritDoc}
      */
@@ -97,11 +98,19 @@ public class LocalFileListFragment extends ExtendedListFragment {
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         Log_OC.i(TAG, "onCreateView() start");
         View v = super.onCreateView(inflater, container, savedInstanceState);
-        setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+
+        if(!mContainerActivity.isFolderPickerMode()) {
+            setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+            setMessageForEmptyList(R.string.file_list_empty_headline, R.string.local_file_list_empty,
+                    R.drawable.ic_list_empty_folder, true);
+        } else {
+            setMessageForEmptyList(R.string.folder_list_empty_headline, R.string.local_folder_list_empty,
+                    R.drawable.ic_list_empty_folder, true);
+        }
+
         setSwipeEnabled(false); // Disable pull-to-refresh
         setFabEnabled(false); // Disable FAB
-        setMessageForEmptyList(R.string.file_list_empty_headline, R.string.local_file_list_empty,
-                R.drawable.ic_list_empty_folder, true);
+
         Log_OC.i(TAG, "onCreateView() end");
         return v;
     }
@@ -115,11 +124,29 @@ public class LocalFileListFragment extends ExtendedListFragment {
         Log_OC.i(TAG, "onActivityCreated() start");
         
         super.onActivityCreated(savedInstanceState);
-        mAdapter = new LocalFileListAdapter(mContainerActivity.getInitialDirectory(), getActivity());
+
+        mAdapter = new LocalFileListAdapter(
+                mContainerActivity.isFolderPickerMode(),
+                mContainerActivity.getInitialDirectory(),
+                getActivity()
+        );
         setListAdapter(mAdapter);
         
         Log_OC.i(TAG, "onActivityCreated() stop");
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        if (mContainerActivity.isFolderPickerMode()) {
+            menu.removeItem(R.id.action_select_all);
+            menu.removeItem(R.id.action_search);
+        } else {
+            super.onCreateOptionsMenu(menu, inflater);
+        }
+    }
     
     /**
      * Checks the file clicked over. Browses inside if it is a directory.
@@ -303,8 +330,7 @@ public class LocalFileListFragment extends ExtendedListFragment {
          * @param file
          */
         void onFileClick(File file);
-        
-        
+
         /**
          * Callback method invoked when the parent activity
          * is fully created to get the directory to list firstly.
@@ -313,6 +339,13 @@ public class LocalFileListFragment extends ExtendedListFragment {
          */
         File getInitialDirectory();
 
+        /**
+         * config check if the list should behave in
+         * folder picker mode only displaying folders but no files.
+         *
+         * @return true if folder picker mode, else false
+         */
+        boolean isFolderPickerMode();
     }
 
 

+ 1 - 1
src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactListFragment.java

@@ -67,7 +67,7 @@ import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.services.ContactsImportJob;
+import com.owncloud.android.jobs.ContactsImportJob;
 import com.owncloud.android.ui.TextDrawable;
 import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
 import com.owncloud.android.ui.events.VCardToggleEvent;

+ 2 - 2
src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactsBackupFragment.java

@@ -49,9 +49,9 @@ import com.owncloud.android.R;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.jobs.ContactsBackupJob;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.operations.RefreshFolderOperation;
-import com.owncloud.android.services.ContactsBackupJob;
 import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
 import com.owncloud.android.ui.activity.Preferences;
 import com.owncloud.android.ui.fragment.FileFragment;
@@ -338,7 +338,7 @@ public class ContactsBackupFragment extends FileFragment implements DatePickerDi
                     contactsPreferenceActivity.getAccount());
         }
 
-        arbitraryDataProvider.storeOrUpdateKeyValue(account, PREFERENCE_CONTACTS_AUTOMATIC_BACKUP,
+        arbitraryDataProvider.storeOrUpdateKeyValue(account.name, PREFERENCE_CONTACTS_AUTOMATIC_BACKUP,
                 String.valueOf(bool));
     }
 

+ 1 - 1
src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java

@@ -176,7 +176,7 @@ public class FileOperationsHelper {
             openFileWithIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
 
             List<ResolveInfo> launchables = mFileActivity.getPackageManager().
-                    queryIntentActivities(openFileWithIntent, PackageManager.GET_INTENT_FILTERS);
+                    queryIntentActivities(openFileWithIntent, PackageManager.GET_RESOLVED_FILTER);
 
             if (launchables != null && launchables.size() > 0) {
                 try {

+ 3 - 1
src/main/java/com/owncloud/android/ui/helpers/UriUploader.java

@@ -166,7 +166,9 @@ public class UriUploader {
                 mBehaviour,
                 null,       // MIME type will be detected from file name
                 false,      // do not create parent folder if not existent
-                UploadFileOperation.CREATED_BY_USER
+                UploadFileOperation.CREATED_BY_USER,
+                false,
+                false
         );
     }
 

+ 2 - 4
src/main/java/com/owncloud/android/ui/preview/PreviewTextFragment.java

@@ -183,7 +183,7 @@ public class PreviewTextFragment extends FileFragment {
     }
 
     private void loadAndShowTextPreview() {
-        mTextLoadTask = new TextLoadAsyncTask(new WeakReference<TextView>(mTextPreview));
+        mTextLoadTask = new TextLoadAsyncTask(new WeakReference<>(mTextPreview));
         mTextLoadTask.execute(getFile().getStoragePath());
     }
 
@@ -192,14 +192,12 @@ public class PreviewTextFragment extends FileFragment {
      * Reads the file to preview and shows its contents. Too critical to be anonymous.
      */
     private class TextLoadAsyncTask extends AsyncTask<Object, Void, StringWriter> {
-        private static final String DIALOG_WAIT_TAG = "DIALOG_WAIT";
         private final WeakReference<TextView> mTextViewReference;
 
         private TextLoadAsyncTask(WeakReference<TextView> textView) {
             mTextViewReference = textView;
         }
 
-
         @Override
         protected void onPreExecute() {
             // not used at the moment
@@ -454,7 +452,7 @@ public class PreviewTextFragment extends FileFragment {
      * @return 'True' if the file can be handled by the fragment.
      */
     public static boolean canBePreviewed(OCFile file) {
-        final List<String> unsupportedTypes = new LinkedList<String>();
+        final List<String> unsupportedTypes = new LinkedList<>();
         unsupportedTypes.add("text/richtext");
         unsupportedTypes.add("text/rtf");
         unsupportedTypes.add("text/vnd.abc");

+ 22 - 5
src/main/java/com/owncloud/android/utils/BitmapUtils.java

@@ -1,4 +1,4 @@
-/**
+/*
  * ownCloud Android client application
  *
  * @author David A. Velasco
@@ -23,7 +23,7 @@ import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.BitmapFactory.Options;
 import android.graphics.Matrix;
-import android.media.ExifInterface;
+import android.support.media.ExifInterface;
 import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
 import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
 
@@ -48,7 +48,7 @@ public class BitmapUtils {
      * @param srcPath       Absolute path to the file containing the image.
      * @param reqWidth      Width of the surface where the Bitmap will be drawn on, in pixels.
      * @param reqHeight     Height of the surface where the Bitmap will be drawn on, in pixels.
-     * @return
+     * @return decoded bitmap
      */
     public static Bitmap decodeSampledBitmapFromFile(String srcPath, int reqWidth, int reqHeight) {
 
@@ -106,6 +106,23 @@ public class BitmapUtils {
         return inSampleSize;
     }
 
+    /**
+     * scales a given bitmap depending on the given size parameters.
+     *
+     * @param bitmap the bitmap to be scaled
+     * @param px     the target pixel size
+     * @param width  the width
+     * @param height the height
+     * @param max    the max(height, width)
+     * @return the scaled bitmap
+     */
+    public static Bitmap scaleBitmap(Bitmap bitmap, float px, int width, int height, int max) {
+        float scale = px / max;
+        int w = Math.round(scale * width);
+        int h = Math.round(scale * height);
+        return Bitmap.createScaledBitmap(bitmap, w, h, true);
+    }
+    
     /**
      * Rotate bitmap according to EXIF orientation. 
      * Cf. http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/ 
@@ -172,7 +189,7 @@ public class BitmapUtils {
      *  @param h Hue is specified as degrees in the range 0 - 360.
      *  @param s Saturation is specified as a percentage in the range 1 - 100.
      *  @param l Lumanance is specified as a percentage in the range 1 - 100.
-     *  @paran alpha  the alpha value between 0 - 1
+     *  @param alpha  the alpha value between 0 - 1
      *  adapted from https://svn.codehaus.org/griffon/builders/gfxbuilder/tags/GFXBUILDER_0.2/
      *  gfxbuilder-core/src/main/com/camick/awt/HSLColor.java
      */
@@ -200,7 +217,7 @@ public class BitmapUtils {
         s /= 100f;
         l /= 100f;
 
-        float q = 0;
+        float q;
 
         if (l < 0.5) {
             q = l * (1 + s);

+ 5 - 0
src/main/java/com/owncloud/android/utils/DisplayUtils.java

@@ -389,6 +389,11 @@ public class DisplayUtils {
     public static SpannableStringBuilder createTextWithSpan(String text, String spanText, StyleSpan style) {
         SpannableStringBuilder sb = new SpannableStringBuilder(text);
         int start = text.lastIndexOf(spanText);
+
+        if (start < 0) {
+            start++;
+        }
+
         int end = start + spanText.length();
         sb.setSpan(style, start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
         return sb;

+ 329 - 0
src/main/java/com/owncloud/android/utils/FilesSyncHelper.java

@@ -0,0 +1,329 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2017 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.utils;
+
+import android.accounts.Account;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.MediaStore;
+import android.support.annotation.RequiresApi;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.evernote.android.job.JobRequest;
+import com.owncloud.android.MainApp;
+import com.owncloud.android.authentication.AccountUtils;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
+import com.owncloud.android.datamodel.FilesystemDataProvider;
+import com.owncloud.android.datamodel.MediaFolderType;
+import com.owncloud.android.datamodel.SyncedFolder;
+import com.owncloud.android.datamodel.SyncedFolderProvider;
+import com.owncloud.android.datamodel.UploadsStorageManager;
+import com.owncloud.android.db.OCUpload;
+import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.jobs.FilesSyncJob;
+import com.owncloud.android.jobs.NContentObserverJob;
+
+import org.lukhnos.nnio.file.FileVisitResult;
+import org.lukhnos.nnio.file.Files;
+import org.lukhnos.nnio.file.Path;
+import org.lukhnos.nnio.file.Paths;
+import org.lukhnos.nnio.file.SimpleFileVisitor;
+import org.lukhnos.nnio.file.attribute.BasicFileAttributes;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/*
+    Various utilities that make auto upload tick
+ */
+public class FilesSyncHelper {
+    public static final String TAG = "FileSyncHelper";
+
+    public static final String GLOBAL = "global";
+    public static final String SYNCEDFOLDERINITIATED = "syncedFolderIntitiated_";
+
+    public static int ContentSyncJobId = 315;
+
+    public static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder) {
+        final Context context = MainApp.getAppContext();
+        final ContentResolver contentResolver = context.getContentResolver();
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(contentResolver);
+
+        Long currentTime = System.currentTimeMillis();
+        double currentTimeInSeconds = currentTime / 1000.0;
+        String currentTimeString = Long.toString((long) currentTimeInSeconds);
+
+        String syncedFolderInitiatedKey = SYNCEDFOLDERINITIATED + syncedFolder.getId();
+        boolean dryRun = TextUtils.isEmpty(arbitraryDataProvider.getValue
+                (GLOBAL, syncedFolderInitiatedKey));
+
+        if (MediaFolderType.IMAGE == syncedFolder.getType()) {
+            if (dryRun) {
+                arbitraryDataProvider.storeOrUpdateKeyValue(GLOBAL, syncedFolderInitiatedKey,
+                        currentTimeString);
+            } else {
+                FilesSyncHelper.insertContentIntoDB(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI
+                        , syncedFolder);
+                FilesSyncHelper.insertContentIntoDB(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                        syncedFolder);
+            }
+
+        } else if (MediaFolderType.VIDEO == syncedFolder.getType()) {
+
+            if (dryRun) {
+                arbitraryDataProvider.storeOrUpdateKeyValue(GLOBAL, syncedFolderInitiatedKey,
+                        currentTimeString);
+            } else {
+                FilesSyncHelper.insertContentIntoDB(android.provider.MediaStore.Video.Media.INTERNAL_CONTENT_URI,
+                        syncedFolder);
+                FilesSyncHelper.insertContentIntoDB(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+                        syncedFolder);
+            }
+
+        } else {
+            try {
+
+                if (dryRun) {
+                    arbitraryDataProvider.storeOrUpdateKeyValue(GLOBAL, syncedFolderInitiatedKey,
+                            currentTimeString);
+                } else {
+                    FilesystemDataProvider filesystemDataProvider = new FilesystemDataProvider(contentResolver);
+                    Path path = Paths.get(syncedFolder.getLocalPath());
+
+                    String dateInitiated = arbitraryDataProvider.getValue(GLOBAL,
+                            syncedFolderInitiatedKey);
+
+                    Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
+                        @Override
+                        public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
+
+                            File file = path.toFile();
+                            if (attrs.lastModifiedTime().toMillis() >= Long.parseLong(dateInitiated) * 1000) {
+                                filesystemDataProvider.storeOrUpdateFileValue(path.toAbsolutePath().toString(),
+                                        attrs.lastModifiedTime().toMillis(), file.isDirectory(), syncedFolder);
+                            }
+
+                            return FileVisitResult.CONTINUE;
+                        }
+
+                        @Override
+                        public FileVisitResult visitFileFailed(Path file, IOException exc) {
+                            return FileVisitResult.CONTINUE;
+                        }
+                    });
+
+                }
+
+            } catch (IOException e) {
+                Log.e(TAG, "Something went wrong while indexing files for auto upload " + e.getLocalizedMessage());
+            }
+        }
+    }
+
+    public static void insertAllDBEntries(boolean skipCustom) {
+        final Context context = MainApp.getAppContext();
+        final ContentResolver contentResolver = context.getContentResolver();
+        SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(contentResolver);
+
+        for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
+            if ((syncedFolder.isEnabled()) && ((MediaFolderType.CUSTOM != syncedFolder.getType()) || !skipCustom)) {
+                    insertAllDBEntriesForSyncedFolder(syncedFolder);
+            }
+        }
+    }
+
+    private static void insertContentIntoDB(Uri uri, SyncedFolder syncedFolder) {
+        final Context context = MainApp.getAppContext();
+        final ContentResolver contentResolver = context.getContentResolver();
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(contentResolver);
+
+        Cursor cursor;
+        int column_index_data;
+        int column_index_date_modified;
+
+        final FilesystemDataProvider filesystemDataProvider = new FilesystemDataProvider(contentResolver);
+
+        String contentPath;
+        boolean isFolder;
+
+        String[] projection = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DATE_MODIFIED};
+
+        String path = syncedFolder.getLocalPath();
+        if (!path.endsWith("/")) {
+            path = path + "/%";
+        } else {
+            path = path + "%";
+        }
+
+        String syncedFolderInitiatedKey = SYNCEDFOLDERINITIATED + syncedFolder.getId();
+        String dateInitiated = arbitraryDataProvider.getValue(GLOBAL, syncedFolderInitiatedKey);
+
+        cursor = context.getContentResolver().query(uri, projection, MediaStore.MediaColumns.DATA + " LIKE ?",
+                new String[]{path}, null);
+
+        if (cursor != null) {
+            column_index_data = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA);
+            column_index_date_modified = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED);
+            while (cursor.moveToNext()) {
+                contentPath = cursor.getString(column_index_data);
+                isFolder = new File(contentPath).isDirectory();
+                if (cursor.getLong(column_index_date_modified) >= Long.parseLong(dateInitiated)) {
+                    filesystemDataProvider.storeOrUpdateFileValue(contentPath,
+                            cursor.getLong(column_index_date_modified), isFolder, syncedFolder);
+                }
+            }
+            cursor.close();
+        }
+    }
+
+    public static void restartJobsIfNeeded() {
+        final Context context = MainApp.getAppContext();
+
+        FileUploader.UploadRequester uploadRequester = new FileUploader.UploadRequester();
+
+        boolean accountExists;
+
+        UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(context.getContentResolver(), context);
+        OCUpload[] failedUploads = uploadsStorageManager.getFailedUploads();
+
+        for (OCUpload failedUpload : failedUploads) {
+            accountExists = false;
+
+            // check if accounts still exists
+            for (Account account : AccountUtils.getAccounts(context)) {
+                if (account.name.equals(failedUpload.getAccountName())) {
+                    accountExists = true;
+                    break;
+                }
+            }
+
+            if (!accountExists) {
+                uploadsStorageManager.removeUpload(failedUpload);
+            }
+        }
+
+        uploadRequester.retryFailedUploads(
+                context,
+                null,
+                null
+        );
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    public static boolean isContentObserverJobScheduled() {
+        JobScheduler js = MainApp.getAppContext().getSystemService(JobScheduler.class);
+        List<JobInfo> jobs = js.getAllPendingJobs();
+
+        if (jobs == null || jobs.size() == 0) {
+            return false;
+        }
+
+        for (int i = 0; i < jobs.size(); i++) {
+            if (jobs.get(i).getId() == ContentSyncJobId) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public static void scheduleNJobs(boolean force) {
+        SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(MainApp.getAppContext().
+                getContentResolver());
+
+
+        boolean hasVideoFolders = false;
+        boolean hasImageFolders = false;
+
+        if (syncedFolderProvider.getSyncedFolders() != null) {
+            for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
+                if (MediaFolderType.VIDEO == syncedFolder.getType()) {
+                    hasVideoFolders = true;
+                } else if (MediaFolderType.IMAGE == syncedFolder.getType()) {
+                    hasImageFolders = true;
+                }
+            }
+        }
+
+            if (hasImageFolders || hasVideoFolders) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                    scheduleJobOnN(hasImageFolders, hasVideoFolders, force);
+                }
+            } else {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                    cancelJobOnN();
+                }
+            }
+    }
+
+    public static void scheduleFilesSyncIfNeeded() {
+        // always run this because it also allows us to perform retries of manual uploads
+        new JobRequest.Builder(FilesSyncJob.TAG)
+                .setPeriodic(900000L, 300000L)
+                .setUpdateCurrent(true)
+                .build()
+                .schedule();
+
+        scheduleNJobs(false);
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    private static void cancelJobOnN() {
+        JobScheduler jobScheduler = MainApp.getAppContext().getSystemService(JobScheduler.class);
+        if (isContentObserverJobScheduled()) {
+            jobScheduler.cancel(ContentSyncJobId);
+        }
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    private static void scheduleJobOnN(boolean hasImageFolders, boolean hasVideoFolders,
+                                       boolean force) {
+        JobScheduler jobScheduler = MainApp.getAppContext().getSystemService(JobScheduler.class);
+
+        if ((hasImageFolders || hasVideoFolders) && (!isContentObserverJobScheduled() || force)) {
+            JobInfo.Builder builder = new JobInfo.Builder(ContentSyncJobId, new ComponentName(MainApp.getAppContext(),
+                    NContentObserverJob.class.getName()));
+            builder.addTriggerContentUri(new JobInfo.TriggerContentUri(android.provider.MediaStore.
+                    Images.Media.INTERNAL_CONTENT_URI,
+                    JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
+            builder.addTriggerContentUri(new JobInfo.TriggerContentUri(MediaStore.
+                    Images.Media.EXTERNAL_CONTENT_URI,
+                    JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
+            builder.addTriggerContentUri(new JobInfo.TriggerContentUri(android.provider.MediaStore.
+                    Video.Media.INTERNAL_CONTENT_URI,
+                    JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
+            builder.addTriggerContentUri(new JobInfo.TriggerContentUri(MediaStore.
+                    Video.Media.EXTERNAL_CONTENT_URI,
+                    JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
+            builder.setTriggerContentMaxDelay(1500);
+            jobScheduler.schedule(builder.build());
+        }
+    }
+}
+

+ 3 - 3
src/main/java/com/owncloud/android/utils/MimeTypeUtil.java

@@ -147,7 +147,7 @@ public class MimeTypeUtil {
         } else if (isSharedViaUsers) {
             drawableId = R.drawable.shared_with_me_folder;
         } else {
-            drawableId = R.drawable.ic_menu_archive;
+            drawableId = R.drawable.folder;
         }
 
         return ThemeUtils.tintDrawable(drawableId, ThemeUtils.primaryColor(account));
@@ -442,7 +442,7 @@ public class MimeTypeUtil {
         MIMETYPE_TO_ICON_MAPPING.put("application/yaml", R.drawable.file_code);
         MIMETYPE_TO_ICON_MAPPING.put("application/zip", R.drawable.file_zip);
         MIMETYPE_TO_ICON_MAPPING.put("database", R.drawable.file);
-        MIMETYPE_TO_ICON_MAPPING.put("httpd/unix-directory", R.drawable.ic_menu_archive);
+        MIMETYPE_TO_ICON_MAPPING.put("httpd/unix-directory", R.drawable.folder);
         MIMETYPE_TO_ICON_MAPPING.put("image/svg+xml", R.drawable.file_image);
         MIMETYPE_TO_ICON_MAPPING.put("image/vector", R.drawable.file_image);
         MIMETYPE_TO_ICON_MAPPING.put("text/calendar", R.drawable.file_calendar);
@@ -456,7 +456,7 @@ public class MimeTypeUtil {
         MIMETYPE_TO_ICON_MAPPING.put("text/x-python", R.drawable.file_code);
         MIMETYPE_TO_ICON_MAPPING.put("text/x-shellscript", R.drawable.file_code);
         MIMETYPE_TO_ICON_MAPPING.put("web", R.drawable.file_code);
-        MIMETYPE_TO_ICON_MAPPING.put(MimeType.DIRECTORY, R.drawable.ic_menu_archive);
+        MIMETYPE_TO_ICON_MAPPING.put(MimeType.DIRECTORY, R.drawable.folder);
     }
 
     /**

+ 74 - 0
src/main/java/com/owncloud/android/utils/ReceiversHelper.java

@@ -0,0 +1,74 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2017 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.utils;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import com.evernote.android.job.JobRequest;
+import com.evernote.android.job.util.Device;
+import com.owncloud.android.MainApp;
+
+/*
+    Helper for setting up network and power receivers
+ */
+public class ReceiversHelper {
+
+    public static void registerNetworkChangeReceiver() {
+        Context context = MainApp.getAppContext();
+
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
+        intentFilter.addAction("android.net.wifi.STATE_CHANGE");
+
+        BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (Device.getNetworkType(context).equals(JobRequest.NetworkType.UNMETERED)) {
+                    FilesSyncHelper.restartJobsIfNeeded();
+                }
+            }
+        };
+
+        context.registerReceiver(broadcastReceiver, intentFilter);
+    }
+
+    public static void registerPowerChangeReceiver() {
+        Context context = MainApp.getAppContext();
+
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_POWER_CONNECTED);
+        intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);
+
+        BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (intent.getAction().equals(Intent.ACTION_POWER_CONNECTED)) {
+                    FilesSyncHelper.restartJobsIfNeeded();
+                }
+            }
+        };
+
+        context.registerReceiver(broadcastReceiver, intentFilter);
+    }
+}

+ 2 - 1
src/main/java/com/owncloud/android/utils/ThemeUtils.java

@@ -37,6 +37,7 @@ import android.support.v4.content.ContextCompat;
 import android.support.v4.content.res.ResourcesCompat;
 import android.support.v4.graphics.ColorUtils;
 import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v4.widget.CompoundButtonCompat;
 import android.support.v7.app.ActionBar;
 import android.support.v7.widget.AppCompatCheckBox;
 import android.support.v7.widget.SwitchCompat;
@@ -254,7 +255,7 @@ public class ThemeUtils {
     }
 
     public static void tintCheckbox(AppCompatCheckBox checkBox, int color) {
-        checkBox.setSupportButtonTintList(new ColorStateList(
+        CompoundButtonCompat.setButtonTintList(checkBox, new ColorStateList(
                 new int[][]{
                         new int[]{-android.R.attr.state_checked},
                         new int[]{android.R.attr.state_checked},

+ 3 - 1
src/main/java/third_parties/in/srain/cube/GridViewWithHeaderAndFooter.java

@@ -273,7 +273,9 @@ public class GridViewWithHeaderAndFooter extends GridView {
                 Field numColumns = getClass().getSuperclass().getDeclaredField("mColumnWidth");
                 numColumns.setAccessible(true);
                 return numColumns.getInt(this);
-            } catch (NoSuchFieldException | IllegalAccessException e) {
+            } catch (NoSuchFieldException e) {
+                throw new RuntimeException(e);
+            } catch (IllegalAccessException e) {
                 throw new RuntimeException(e);
             }
         }

BIN
src/main/res/drawable-hdpi/common_error.png


+ 0 - 0
src/main/res/drawable-hdpi/ic_menu_archive.png → src/main/res/drawable-hdpi/folder.png


BIN
src/main/res/drawable-hdpi/ic_action_cancel_sync.png


BIN
src/main/res/drawable-hdpi/ic_action_cancel_white.png


BIN
src/main/res/drawable-hdpi/ic_action_copy.png


BIN
src/main/res/drawable-hdpi/ic_action_delete_white.png


BIN
src/main/res/drawable-hdpi/ic_action_download.png


BIN
src/main/res/drawable-hdpi/ic_action_move.png


BIN
src/main/res/drawable-hdpi/ic_action_set_available_offline.png


BIN
src/main/res/drawable-hdpi/ic_action_set_available_offline_white.png


BIN
src/main/res/drawable-hdpi/ic_action_share.png


BIN
src/main/res/drawable-hdpi/ic_action_unset_available_offline.png


BIN
src/main/res/drawable-hdpi/ic_action_unset_available_offline_white.png


BIN
src/main/res/drawable-hdpi/ic_cellphone.png


Some files were not shown because too many files changed in this diff