Procházet zdrojové kódy

Merge branch 'master' into sso

David Luhmer před 7 roky
rodič
revize
e08cb4dec4
100 změnil soubory, kde provedl 2336 přidání a 1401 odebrání
  1. 4 0
      .codecov.yml
  2. 9 12
      .drone.yml
  3. 10 0
      CHANGELOG.md
  4. 9 2
      CONTRIBUTING.md
  5. 24 22
      build.gradle
  6. 4 0
      drawable_resources/history.svg
  7. 1 0
      drawable_resources/ic_content-copy.svg
  8. 53 0
      drawable_resources/ic_delete.svg
  9. 4 0
      drawable_resources/ic_history.svg
  10. 10 0
      fastlane/metadata/android/en-US/changelogs/30020199.txt
  11. 4 1
      findbugs-filter.xml
  12. 4 0
      gplay.gradle
  13. 1 0
      gradle/wrapper/gradle-wrapper.properties
  14. 68 0
      scripts/analysis/analysis-wrapper.sh
  15. 0 0
      scripts/analysis/getBranchName.sh
  16. 2 0
      scripts/analysis/lint-results.txt
  17. 1 1
      scripts/analysis/lint-up.rb
  18. 0 2
      scripts/lint/lint-results.txt
  19. 0 36
      scripts/lint/lint-up-wrapper.sh
  20. 0 2
      src/androidTest/java/com/owncloud/android/UploadIT.java
  21. 1 1
      src/androidTest/java/com/owncloud/android/datamodel/OCFileUnitTest.java
  22. 1 0
      src/generic/fastlane/metadata/android/cs-CZ/short_description.txt
  23. 1 0
      src/generic/fastlane/metadata/android/cs-CZ/title.txt
  24. 1 0
      src/generic/fastlane/metadata/android/pl-PL/short_description.txt
  25. 6 2
      src/gplay/AndroidManifest.xml
  26. 1 1
      src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java
  27. 9 0
      src/gplay/res/values/setup.xml
  28. 3 1
      src/main/AndroidManifest.xml
  29. 32 6
      src/main/java/com/owncloud/android/MainApp.java
  30. 2 2
      src/main/java/com/owncloud/android/authentication/AccountAuthenticatorActivity.java
  31. 200 221
      src/main/java/com/owncloud/android/authentication/AccountUtils.java
  32. 56 46
      src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java
  33. 1 1
      src/main/java/com/owncloud/android/authentication/AuthenticatorUrlUtils.java
  34. 3 3
      src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.java
  35. 13 13
      src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadata.java
  36. 5 6
      src/main/java/com/owncloud/android/datamodel/EncryptedFolderMetadata.java
  37. 3 2
      src/main/java/com/owncloud/android/datamodel/ExternalLinksProvider.java
  38. 17 6
      src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  39. 0 3
      src/main/java/com/owncloud/android/datamodel/FilesystemDataProvider.java
  40. 56 0
      src/main/java/com/owncloud/android/datamodel/MediaFoldersModel.java
  41. 6 6
      src/main/java/com/owncloud/android/datamodel/MediaProvider.java
  42. 13 15
      src/main/java/com/owncloud/android/datamodel/OCFile.java
  43. 53 5
      src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java
  44. 132 134
      src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java
  45. 2 2
      src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java
  46. 1 2
      src/main/java/com/owncloud/android/datastorage/DataStorageProvider.java
  47. 136 103
      src/main/java/com/owncloud/android/db/OCUpload.java
  48. 2 2
      src/main/java/com/owncloud/android/features/FeatureList.java
  49. 17 1
      src/main/java/com/owncloud/android/files/FileMenuFilter.java
  50. 91 0
      src/main/java/com/owncloud/android/files/StreamMediaFileOperation.java
  51. 11 11
      src/main/java/com/owncloud/android/files/services/FileDownloader.java
  52. 25 28
      src/main/java/com/owncloud/android/files/services/FileUploader.java
  53. 9 10
      src/main/java/com/owncloud/android/files/services/IndexedForest.java
  54. 57 50
      src/main/java/com/owncloud/android/jobs/ContactsBackupJob.java
  55. 2 3
      src/main/java/com/owncloud/android/jobs/FilesSyncJob.java
  56. 173 0
      src/main/java/com/owncloud/android/jobs/MediaFoldersDetectionJob.java
  57. 2 0
      src/main/java/com/owncloud/android/jobs/NCJobCreator.java
  58. 23 8
      src/main/java/com/owncloud/android/jobs/NContentObserverJob.java
  59. 5 11
      src/main/java/com/owncloud/android/jobs/NotificationJob.java
  60. 8 115
      src/main/java/com/owncloud/android/media/MediaControlView.java
  61. 81 49
      src/main/java/com/owncloud/android/media/MediaService.java
  62. 2 4
      src/main/java/com/owncloud/android/media/MediaServiceBinder.java
  63. 2 2
      src/main/java/com/owncloud/android/operations/CheckCurrentCredentialsOperation.java
  64. 112 0
      src/main/java/com/owncloud/android/operations/CommentFileOperation.java
  65. 3 3
      src/main/java/com/owncloud/android/operations/DownloadFileOperation.java
  66. 83 0
      src/main/java/com/owncloud/android/operations/EmptyTrashbinFileOperation.java
  67. 105 97
      src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java
  68. 29 24
      src/main/java/com/owncloud/android/operations/RemoveFileOperation.java
  69. 1 1
      src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.java
  70. 2 5
      src/main/java/com/owncloud/android/operations/RenameFileOperation.java
  71. 92 0
      src/main/java/com/owncloud/android/operations/RestoreFileVersionOperation.java
  72. 93 0
      src/main/java/com/owncloud/android/operations/RestoreTrashbinFileOperation.java
  73. 1 1
      src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java
  74. 1 2
      src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java
  75. 2 2
      src/main/java/com/owncloud/android/operations/UnshareOperation.java
  76. 21 27
      src/main/java/com/owncloud/android/operations/UploadFileOperation.java
  77. 1 1
      src/main/java/com/owncloud/android/providers/DiskLruImageCacheFileProvider.java
  78. 5 4
      src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java
  79. 33 23
      src/main/java/com/owncloud/android/services/OperationsService.java
  80. 2 5
      src/main/java/com/owncloud/android/services/SyncFolderHandler.java
  81. 1 1
      src/main/java/com/owncloud/android/syncadapter/AbstractOwnCloudSyncAdapter.java
  82. 1 8
      src/main/java/com/owncloud/android/syncadapter/FileSyncAdapter.java
  83. 1 1
      src/main/java/com/owncloud/android/syncadapter/FileSyncService.java
  84. 2 3
      src/main/java/com/owncloud/android/ui/CustomPopup.java
  85. 1 2
      src/main/java/com/owncloud/android/ui/EmptyRecyclerView.java
  86. 4 3
      src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java
  87. 2 1
      src/main/java/com/owncloud/android/ui/activities/ActivitiesContract.java
  88. 3 4
      src/main/java/com/owncloud/android/ui/activities/ActivitiesPresenter.java
  89. 1 2
      src/main/java/com/owncloud/android/ui/activities/data/files/FilesRepository.java
  90. 1 2
      src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApi.java
  91. 3 10
      src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApiImpl.java
  92. 2 3
      src/main/java/com/owncloud/android/ui/activities/data/files/RemoteFilesRepository.java
  93. 6 7
      src/main/java/com/owncloud/android/ui/activity/BaseActivity.java
  94. 6 42
      src/main/java/com/owncloud/android/ui/activity/CopyToClipboardActivity.java
  95. 35 63
      src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
  96. 3 3
      src/main/java/com/owncloud/android/ui/activity/ErrorsWhileCopyingHandlerActivity.java
  97. 8 4
      src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java
  98. 8 7
      src/main/java/com/owncloud/android/ui/activity/FileActivity.java
  99. 183 79
      src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  100. 7 8
      src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.java

+ 4 - 0
.codecov.yml

@@ -7,6 +7,10 @@ coverage:
   precision: 2
   round: down
   range: "70...100"
+  status:
+    project:
+      default:
+        threshold: 0.5
 
 comment:
   layout: "header, diff, changes, uncovered, tree"

+ 9 - 12
.drone.yml

@@ -2,10 +2,8 @@ pipeline:
   compile:
     image: nextcloudci/android:android-35
     commands:
-      # uncomment gplay for Gplay
-      - sh -c "if [ '${FLAVOR}' = 'Gplay' ]; then sed -i '/.*com.google.*/s/^.*\\/\\///g' build.gradle; fi"
       # build app and assemble APK
-      - sh -c "if [ '${FLAVOR}' != 'Lint' ]; then ./gradlew assemble${FLAVOR}; fi"
+      - sh -c "if [ '${FLAVOR}' != 'Analysis' ]; then ./gradlew assemble${FLAVOR}; fi"
     when:
       matrix:
         FLAVOR: [Generic, Gplay]
@@ -16,8 +14,9 @@ pipeline:
     commands:
       - emulator -avd android-27 -no-window -no-audio &
       - ./wait_for_emulator.sh
+      - ./gradlew assembleGplayDebug
       - ./gradlew jacocoTestGplayDebugUnitTestReport || scripts/uploadReport.sh $LOG_USERNAME $LOG_PASSWORD $DRONE_BUILD_NUMBER "Unit"
-      - ./gradlew assembleGplayDebug installGplayDebugAndroidTest
+      - ./gradlew installGplayDebugAndroidTest
       - ./gradlew createGplayDebugCoverageReport || scripts/uploadReport.sh $LOG_USERNAME $LOG_PASSWORD $DRONE_BUILD_NUMBER "IT"
       - ./gradlew combinedTestReport
       - curl -o codecov.sh https://codecov.io/bash
@@ -27,17 +26,15 @@ pipeline:
       matrix:
         FLAVOR: Gplay
 
-  lint:
-    image: nextcloudci/android:android-35
+  analysis:
+    image: nextcloudci/android:android-37
     commands:
-      # needs gplay
-      - sed -i '/.*com.google.*/s/^.*\\/\\///g' build.gradle
-      - export BRANCH=$(scripts/lint/getBranchName.sh $GIT_USERNAME $GIT_TOKEN $DRONE_PULL_REQUEST)
-      - scripts/lint/lint-up-wrapper.sh $GIT_USERNAME $GIT_TOKEN $BRANCH $LOG_USERNAME $LOG_PASSWORD $DRONE_BUILD_NUMBER
+      - export BRANCH=$(scripts/analysis/getBranchName.sh $GIT_USERNAME $GIT_TOKEN $DRONE_PULL_REQUEST)
+      - scripts/analysis/analysis-wrapper.sh $GIT_USERNAME $GIT_TOKEN $BRANCH $LOG_USERNAME $LOG_PASSWORD $DRONE_BUILD_NUMBER $DRONE_PULL_REQUEST
     secrets: [ GIT_USERNAME, GIT_TOKEN, LOG_USERNAME, LOG_PASSWORD ]
     when:
       matrix:
-        FLAVOR: Lint
+        FLAVOR: Analysis
         
   notify:
       image: drillster/drone-email
@@ -58,7 +55,7 @@ matrix:
   FLAVOR:
     - Generic
     - Gplay
-    - Lint
+    - Analysis
 
 services:
   server:

+ 10 - 0
CHANGELOG.md

@@ -1,3 +1,13 @@
+## 3.2.1 (June, 11, 2018)
+- Enhanced file detail/sharing screen for mail-shares
+- Fix local sorting and file selection
+- Fix local filtering
+- Fix back navigation on privacy screen
+- Fix bug on searching
+- Fix crash on sorting
+- Fix wrong menu highlighting
+- various bug fixes
+
 ## 3.2.0 (May, 13, 2018)
 - Revamped details screen & sharing
 - minor UI/UX improvements

+ 9 - 2
CONTRIBUTING.md

@@ -127,6 +127,13 @@ To make sure your new pull request does not contain commits which are already co
 * Push branch to server: ```git push -u origin name_of_local_master_branch```
 * Use GitHub to issue PR
 
+### 4. Backport pull request:
+If some pull request is worth to backport to a dot release, label it as "backport-request".
+
+* create a new branch based on latest stable branch
+* git cherry-pick commits from origin pull request
+* create pull request on github with "Backport of #originPullRequest: description"
+* remove label "backport-request" from origin pull request
 
 ## Translations
 We manage translations via [Transifex](https://www.transifex.com/nextcloud/nextcloud/android/). So just request joining the translation team for Android on the site and start translating. All translations will then be automatically pushed to this repository, there is no need for any pull request for translations.
@@ -178,11 +185,11 @@ beware, that beta releases for an upcoming version will always use the minor and
 For dev the version name is in format YYYYMMDD. It is mainly as a reference for reporting bugs and is not related to stable/release candidates as it is an independent app.
 
 ## Release cycle
-* for each release we choose several PRs that will be included in the next release. Currently there are many open PRs from ownCloud, but after merging them, the intention is to choose the PRs that are ready (reviewed, tested) to get them merged very soon.
-* these will be merged into master, tested heavily, maybe automatic testing
+* Releases are planned every ~2 months, with 6 weeks of developing and 2 weeks of stabilising
 * after feature freeze a public release candidate on play store and f-droid is released
 * ~2 weeks testing, bug fixing
 * release final version on f-droid and play store
+* Bugfix releases (dot releases, e.g. 3.2.1) are released on demand from the branch created with first stable release (stable-3.2.x). If changes to the library are required, we do the same: create a branch from the version used in stable release (e.g. 1.1.0) and then release a dot release (1.1.1).
 
 To get an idea which PRs and issues will be part of the next release simply check our [milestone plan](https://github.com/nextcloud/android/milestones)
 

+ 24 - 22
build.gradle

@@ -12,10 +12,10 @@ buildscript {
             url 'https://oss.sonatype.org/content/repositories/snapshots/'
         }
         google()
+        mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.1.3'
-        classpath 'com.google.gms:google-services:3.0.0'
+        classpath 'com.android.tools.build:gradle:3.1.4'
         classpath 'org.codehaus.groovy:groovy-all:2.4.12' 
         classpath('com.dicedmelon.gradle:jacoco-android:0.1.2') { 
             exclude group: 'org.codehaus.groovy', module: 'groovy-all' 
@@ -36,8 +36,6 @@ configurations.all {
 
 ext {
     supportLibraryVersion = '27.1.1'
-    googleLibraryVersion = '12.0.1'
-    androidLibraryVersion = '1.0.40'
     jacocoVersion = "0.7.4.201502262128"
 
     travisBuild = System.getenv("TRAVIS") == "true"
@@ -63,6 +61,11 @@ def versionMinor = 3
 def versionPatch = 0
 def versionBuild = 0 // 0-49=Alpha / 50-98=RC / 99=stable
 
+def taskRequest = getGradle().getStartParameter().getTaskRequests().toString();
+if (taskRequest.contains("Gplay") || taskRequest.contains("findbugs") || taskRequest.contains("lint")) {
+    apply from: 'gplay.gradle'
+}
+
 android {
     lintOptions {
         abortOnError false
@@ -126,8 +129,8 @@ android {
             versionDev {
                 applicationId "com.nextcloud.android.beta"
                 dimension "default"
-                versionCode 20180613
-                versionName "20180613"
+                versionCode 20180824
+                versionName "20180824"
             }
         }
     }
@@ -178,10 +181,12 @@ android {
     task findbugs(type: FindBugs) {
         ignoreFailures = false
         effort = "max"
-        reportLevel = "high"
-        classes = files("$project.buildDir/intermediates/classes")
-        excludeFilter = new File("${project.rootDir}/findbugs-filter.xml")
-        source 'src'
+        reportLevel = "medium"
+        classes = fileTree("$project.buildDir/intermediates/classes/gplay/debug/com/owncloud")
+        excludeFilter = file("${project.rootDir}/findbugs-filter.xml")
+        source = fileTree('src/main/java')
+        pluginClasspath = project.configurations.findbugsPlugins
+        classpath = files()
         include '**/*.java'
         exclude '**/gen/**'
 
@@ -189,11 +194,11 @@ android {
             xml.enabled = false
             html.enabled = true
             html {
-                destination  = file("$project.buildDir/reports/findbugs/findbugs.html")
+                destination = file("$project.buildDir/reports/findbugs/findbugs.html")
             }
         }
-        classpath = files()
     }
+
     check.dependsOn 'checkstyle', 'findbugs', 'pmd', 'lint'
 
     compileOptions {
@@ -222,29 +227,26 @@ dependencies {
     implementation 'com.jakewharton:butterknife:8.8.1'
     annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
     implementation 'org.greenrobot:eventbus:3.0.0'
-    implementation 'com.googlecode.ez-vcard:ez-vcard:0.10.2'
+    implementation 'com.googlecode.ez-vcard:ez-vcard:0.10.4'
     implementation 'org.lukhnos:nnio:0.2'
     implementation 'com.madgag.spongycastle:pkix:1.54.0.0'
     implementation 'com.google.code.gson:gson:2.8.2'
     implementation 'com.afollestad:sectioned-recyclerview:0.5.0'
     implementation 'com.github.chrisbanes:PhotoView:2.1.3'
-    implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.12'
+    implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.15'
 
-    // uncomment for gplay
-    // implementation "com.google.firebase:firebase-messaging:${googleLibraryVersion}"
-    // implementation "com.google.android.gms:play-services-gcm:${googleLibraryVersion}"
     implementation 'org.parceler:parceler-api:1.1.9'
     annotationProcessor 'org.parceler:parceler:1.1.9'
     implementation ('com.github.bumptech.glide:glide:3.7.0') {
         exclude group: "com.android.support"
     }
-    implementation 'com.caverock:androidsvg:1.2.1'
+    implementation 'com.caverock:androidsvg:1.3'
     implementation "com.android.support:support-annotations:${supportLibraryVersion}"
     implementation 'com.google.code.gson:gson:2.8.2'
 
     // dependencies for local unit tests
     testImplementation 'junit:junit:4.12'
-    testImplementation 'org.mockito:mockito-core:1.10.19'
+    testImplementation 'org.mockito:mockito-core:2.2.5'
     // dependencies for instrumented tests
     // JUnit4 Rules
     androidTestImplementation 'com.android.support.test:rules:1.0.2'
@@ -261,6 +263,9 @@ dependencies {
     implementation 'org.jetbrains:annotations:15.0'
 
     androidTestImplementation 'tools.fastlane:screengrab:1.0.0'
+
+    findbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.4.4'
+    findbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.4.2'
     
 //    jacocoAnt "org.jacoco:org.jacoco.ant:${jacocoVersion}"
 //    jacocoAgent "org.jacoco:org.jacoco.agent:${jacocoVersion}"
@@ -305,6 +310,3 @@ task combinedTestReport(type: JacocoReport) {
             'jacoco/testGplayDebugUnitTest.exec', 'outputs/code-coverage/connected/flavors/GPLAY/*coverage.ec'
     ])
 }
-
-    // uncomment for gplay (must be at the bottom)
-    // apply plugin: 'com.google.gms.google-services'

+ 4 - 0
drawable_resources/history.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewbox="0 0 16 16" width="16" height="16">
+    <path
+        d="m9.025 1.08c-3.95 0-6.535 3.447-6.364 6.72h-2.161l3.904 3.92 4.08-3.874h-2.147c-0.237-1.7 1.163-3.114 2.689-3.092 1.595 0.024 2.8 1.23 2.8 2.734 0.09 1.594-1.63 3.428-3.966 2.53 0 1.23 0.003 2.545 0 3.765 4.19 0.83 7.64-2.51 7.64-6.25 0-3.563-2.92-6.453-6.475-6.453z"/>
+</svg>

+ 1 - 0
drawable_resources/ic_content-copy.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="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" /></svg>

+ 53 - 0
drawable_resources/ic_delete.svg

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+    xmlns:dc="http://purl.org/dc/elements/1.1/"
+    xmlns:cc="http://creativecommons.org/ns#"
+    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+    xmlns="http://www.w3.org/2000/svg"
+    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+    version="1.1"
+    viewbox="0 0 16 16"
+    width="16"
+    height="16"
+    id="svg4"
+    sodipodi:docname="ic_delete.svg"
+    inkscape:version="0.92.2 2405546, 2018-03-11">
+    <metadata
+        id="metadata10">
+        <rdf:RDF>
+            <cc:Work
+                rdf:about="">
+                <dc:format>image/svg+xml</dc:format>
+                <dc:type
+                    rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+            </cc:Work>
+        </rdf:RDF>
+    </metadata>
+    <defs
+        id="defs8"/>
+    <sodipodi:namedview
+        pagecolor="#ffffff"
+        bordercolor="#666666"
+        borderopacity="1"
+        objecttolerance="10"
+        gridtolerance="10"
+        guidetolerance="10"
+        inkscape:pageopacity="0"
+        inkscape:pageshadow="2"
+        inkscape:window-width="1600"
+        inkscape:window-height="835"
+        id="namedview6"
+        showgrid="false"
+        inkscape:zoom="14.75"
+        inkscape:cx="-3.8305085"
+        inkscape:cy="8"
+        inkscape:window-x="0"
+        inkscape:window-y="0"
+        inkscape:window-maximized="1"
+        inkscape:current-layer="svg4"/>
+    <path
+        d="M6.5 1L6 2H3c-.554 0-1 .446-1 1v1h12V3c0-.554-.446-1-1-1h-3l-.5-1zM3 5l.875 9c.06.55.573 1 1.125 1h6c.552 0 1.064-.45 1.125-1L13 5z"
+        id="path2"
+        style="fill:#c4c4c4;fill-opacity:1"/>
+</svg>

+ 4 - 0
drawable_resources/ic_history.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewbox="0 0 16 16" width="16" height="16">
+    <path
+        d="m9.025 1.08c-3.95 0-6.535 3.447-6.364 6.72h-2.161l3.904 3.92 4.08-3.874h-2.147c-0.237-1.7 1.163-3.114 2.689-3.092 1.595 0.024 2.8 1.23 2.8 2.734 0.09 1.594-1.63 3.428-3.966 2.53 0 1.23 0.003 2.545 0 3.765 4.19 0.83 7.64-2.51 7.64-6.25 0-3.563-2.92-6.453-6.475-6.453z"/>
+</svg>

+ 10 - 0
fastlane/metadata/android/en-US/changelogs/30020199.txt

@@ -0,0 +1,10 @@
+3.2.1 (June, 11, 2018)
+
+Enhanced file detail/sharing screen for mail-shares
+Fix local sorting and file selection
+Fix local filtering
+Fix back navigation on privacy screen
+Fix bug on searching
+Fix crash on sorting
+Fix wrong menu highlighting
+various bug fixes

+ 4 - 1
findbugs-filter.xml

@@ -5,4 +5,7 @@
     <Match>
         <Class name="~.*\.Manifest\$.*"/>
     </Match>
-</FindBugsFilter>
+    <Match>
+        <Class name="~.*\.R\$.*"/>
+    </Match>
+</FindBugsFilter>

+ 4 - 0
gplay.gradle

@@ -0,0 +1,4 @@
+dependencies {
+    implementation "com.google.firebase:firebase-messaging:17.1.0"
+    implementation "com.google.android.gms:play-services-gcm:15.0.1"
+}

+ 1 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -4,3 +4,4 @@ distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
 distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
+distributionSha256Sum=7a2c66d1a78f811d5f37d14630ad21cec5e77a2a4dc61e787e2257a6341016ce

+ 68 - 0
scripts/analysis/analysis-wrapper.sh

@@ -0,0 +1,68 @@
+#!/bin/sh
+
+#1: GIT_USERNAME
+#2: GIT_TOKEN
+#3: BRANCH
+#4: LOG_USERNAME
+#5: LOG_PASSWORD
+#6: DRONE_BUILD_NUMBER
+#7: PULL_REQUEST_NUMBER
+
+ruby scripts/analysis/lint-up.rb $1 $2 $3
+lintValue=$?
+
+./gradlew findbugs
+
+# exit codes:
+# 0: count was reduced
+# 1: count was increased
+# 2: count stayed the same
+
+echo "Branch: $3"
+
+if [ $3 = "master" ]; then
+    echo "New findbugs result for master at: https://nextcloud.kaminsky.me/index.php/s/fYZa7NeBsnmFZQD"
+    curl -u $4:$5 -X PUT https://nextcloud.kaminsky.me/remote.php/webdav/findbugs/master.html --upload-file build/reports/findbugs/findbugs.html
+    
+    summary=$(sed -n "/<h1>Summary<\/h1>/,/<h1>Warnings<\/h1>/p" build/reports/findbugs/findbugs.html | head -n-1 | sed s'/<\/a>//'g | sed s'/<a.*>//'g | sed s'/Summary/FindBugs (master)/' | tr "\"" "\'" | tr -d "\r\n")
+    curl -u $4:$5 -X PUT -d "$summary" https://nextcloud.kaminsky.me/remote.php/webdav/findbugs/findbugs.html
+    
+    if [ $lintValue -ne 1 ]; then
+        echo "New lint result for master at: https://nextcloud.kaminsky.me/index.php/s/tXwtChzyqMj6I8v"
+        curl -u $4:$5 -X PUT https://nextcloud.kaminsky.me/remote.php/webdav/droneLogs/master.html --upload-file build/reports/lint/lint.html
+        exit 0
+    fi
+else
+    if [ -e $6 ]; then
+        6="master-"$(date +%F)
+    fi
+    echo "New lint results at https://nextcloud.kaminsky.me/index.php/s/tXwtChzyqMj6I8v ->" $6.html
+    curl 2>/dev/null -u $4:$5 -X PUT https://nextcloud.kaminsky.me/remote.php/webdav/droneLogs/$6.html --upload-file build/reports/lint/lint.html
+    
+    echo "New findbugs results at https://nextcloud.kaminsky.me/index.php/s/fYZa7NeBsnmFZQD ->" $6.html
+    curl 2>/dev/null -u $4:$5 -X PUT https://nextcloud.kaminsky.me/remote.php/webdav/findbugs/$6.html --upload-file build/reports/findbugs/findbugs.html
+    
+    # delete all old comments
+    oldComments=$(curl 2>/dev/null -u $1:$2 -X GET https://api.github.com/repos/nextcloud/android/issues/$7/comments | jq '.[] | (.id |tostring)  + "|" + (.user.login | test("nextcloud-android-bot") | tostring) ' | grep true | tr -d "\"" | cut -f1 -d"|")
+    
+    echo $oldComments | while read comment ; do 
+        curl 2>/dev/null -u $1:$2 -X DELETE https://api.github.com/repos/nextcloud/android/issues/comments/$comment
+    done
+    
+    # add comment with results
+    lintResultNew=$(grep "Lint Report.* [0-9]* warnings" build/reports/lint/lint.html | cut -f2 -d':' |cut -f1 -d'<')
+    lintErrorNew=$(echo $lintResultNew | grep  "[0-9]* error" -o | cut -f1 -d" ")
+    lintWarningNew=$(echo $lintResultNew | grep  "[0-9]* warning" -o | cut -f1 -d" ")
+    lintErrorOld=$(grep "[0-9]* error" scripts/analysis/lint-results.txt -o | cut -f1 -d" ")
+    lintWarningOld=$(grep "[0-9]* warning" scripts/analysis/lint-results.txt -o | cut -f1 -d" ")
+    lintResult="<h1>Lint</h1><table width='500' cellpadding='5' cellspacing='2'><tr class='tablerow0'><td>Type</td><td>Master</td><td>PR</td></tr><tr class='tablerow1'><td>Warnings</td><td>"$lintWarningOld"</td><td>"$lintWarningNew"</td></tr><tr class='tablerow0'><td>Errors</td><td>"$lintErrorOld"</td><td>"$lintErrorNew"</td></tr></table>"
+    findbugsResultNew=$(sed -n "/<h1>Summary<\/h1>/,/<h1>Warnings<\/h1>/p" build/reports/findbugs/findbugs.html |head -n-1 | sed s'/<\/a>//'g | sed s'/<a.*>//'g | sed s'/Summary/FindBugs (new)/' | tr "\"" "\'" | tr -d "\n")
+    findbugsResultOld=$(curl 2>/dev/null https://nextcloud.kaminsky.me/index.php/s/YaHngKMM6ERmBeg/download | tr "\"" "\'" | tr -d "\r\n")
+    curl -u $1:$2 -X POST https://api.github.com/repos/nextcloud/android/issues/$7/comments -d "{ \"body\" : \"$lintResult $findbugsResultNew $findbugsResultOld \" }"
+    
+    if [ $lintValue -eq 2 ]; then
+        exit 0
+    else
+        exit $lintValue
+    fi  
+fi

+ 0 - 0
scripts/lint/getBranchName.sh → scripts/analysis/getBranchName.sh


+ 2 - 0
scripts/analysis/lint-results.txt

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

+ 1 - 1
scripts/lint/lint-up.rb → scripts/analysis/lint-up.rb

@@ -21,7 +21,7 @@ TRAVIS_GIT_USERNAME = String.new("Drone CI server")
 LINT_REPORT_FILE = String.new("build/reports/lint/lint.html")
 
 # File name and relative path of previous results of this script.
-PREVIOUS_LINT_RESULTS_FILE=String.new("scripts/lint/lint-results.txt")
+PREVIOUS_LINT_RESULTS_FILE=String.new("scripts/analysis/lint-results.txt")
 
 # Flag to evaluate warnings. true = check warnings; false = ignore warnings
 CHECK_WARNINGS = true

+ 0 - 2
scripts/lint/lint-results.txt

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

+ 0 - 36
scripts/lint/lint-up-wrapper.sh

@@ -1,36 +0,0 @@
-#!/bin/sh
-
-#1: GIT_USERNAME
-#2: GIT_TOKEN
-#3: BRANCH
-#4: LOG_USERNAME
-#5: LOG_PASSWORD
-#6: DRONE_BUILD_NUMBER
-
-ruby scripts/lint/lint-up.rb $1 $2 $3
-returnValue=$?
-
-# exit codes:
-# 0: count was reduced
-# 1: count was increased
-# 2: count stayed the same
-
-echo "Branch: $3"
-
-if [ $3 = "master" -a $returnValue -ne 1 ]; then
-    echo "New master at: https://nextcloud.kaminsky.me/index.php/s/tXwtChzyqMj6I8v"
-    curl -u $4:$5 -X PUT https://nextcloud.kaminsky.me/remote.php/webdav/droneLogs/master.html --upload-file build/reports/lint/lint.html
-    exit 0
-else
-    if [ -e $6 ]; then
-        6="master-"$(date +%F)
-    fi
-    echo "New results at https://nextcloud.kaminsky.me/index.php/s/tXwtChzyqMj6I8v ->" $6.html
-    curl -u $4:$5 -X PUT https://nextcloud.kaminsky.me/remote.php/webdav/droneLogs/$6.html --upload-file build/reports/lint/lint.html
-    
-    if [ $returnValue -eq 2 ]; then
-        exit 0
-    else
-        exit $returnValue
-    fi  
-fi

+ 0 - 2
src/androidTest/java/com/owncloud/android/UploadIT.java

@@ -27,7 +27,6 @@ public class UploadIT extends AbstractIT {
                 null,
                 ocUpload,
                 false,
-                false,
                 FileUploader.LOCAL_BEHAVIOUR_COPY,
                 context,
                 false,
@@ -52,7 +51,6 @@ public class UploadIT extends AbstractIT {
                 null,
                 ocUpload,
                 false,
-                false,
                 FileUploader.LOCAL_BEHAVIOUR_COPY,
                 context,
                 false,

+ 1 - 1
src/androidTest/java/com/owncloud/android/datamodel/OCFileUnitTest.java

@@ -109,7 +109,7 @@ public class OCFileUnitTest {
         assertThat(fileReadFromParcel.getFileId(), is(ID));
         assertThat(fileReadFromParcel.getParentId(), is(PARENT_ID));
         assertThat(fileReadFromParcel.getStoragePath(), is(STORAGE_PATH));
-        assertThat(fileReadFromParcel.getMimetype(), is(MIME_TYPE));
+        assertThat(fileReadFromParcel.getMimeType(), is(MIME_TYPE));
         assertThat(fileReadFromParcel.getFileLength(), is(FILE_LENGTH));
         assertThat(fileReadFromParcel.getCreationTimestamp(), is(CREATION_TIMESTAMP));
         assertThat(fileReadFromParcel.getModificationTimestamp(), is(MODIFICATION_TIMESTAMP));

+ 1 - 0
src/generic/fastlane/metadata/android/cs-CZ/short_description.txt

@@ -0,0 +1 @@
+Aplikace Nextcloud pro Android ti nabízí přístup ke všem souborům ve tvém Nextcl

+ 1 - 0
src/generic/fastlane/metadata/android/cs-CZ/title.txt

@@ -0,0 +1 @@
+Nextcloud

+ 1 - 0
src/generic/fastlane/metadata/android/pl-PL/short_description.txt

@@ -0,0 +1 @@
+Przy użyciu aplikacji mobilnej Nextcloud na Android'a uzyskasz dostęp do wszystk

+ 6 - 2
src/gplay/AndroidManifest.xml

@@ -28,8 +28,12 @@
         android:theme="@style/Theme.ownCloud.Toolbar"
         android:manageSpaceActivity="com.owncloud.android.ui.activity.ManageSpaceActivity">
 
-        <meta-data android:name="com.google.android.gms.version"
-                   android:value="@integer/google_play_services_version" />
+        <meta-data
+            android:name="firebase_analytics_collection_deactivated"
+            android:value="true"/>
+        <meta-data
+            android:name="google_analytics_adid_collection_enabled"
+            android:value="false"/>
 
         <activity
             android:name=".authentication.ModifiedAuthenticatorActivity"

+ 1 - 1
src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java

@@ -29,7 +29,7 @@ public class NCFirebaseMessagingService extends FirebaseMessagingService {
 
     @Override
     public void onMessageReceived(RemoteMessage remoteMessage) {
-        if (remoteMessage.getData() != null) {
+        if (remoteMessage != null && remoteMessage.getData() != null) {
             PersistableBundleCompat persistableBundleCompat = new PersistableBundleCompat();
             persistableBundleCompat.putString(NotificationJob.KEY_NOTIFICATION_SUBJECT, remoteMessage.getData().get
                     (NotificationJob.KEY_NOTIFICATION_SUBJECT));

+ 9 - 0
src/gplay/res/values/setup.xml

@@ -2,6 +2,15 @@
 <resources>
     <!-- Push server url -->
     <string name="push_server_url" translatable="false">https://push-notifications.nextcloud.com</string>
+
+    <string name="default_web_client_id" translatable="false">829118773643-cq33cmhv7mnv7iq8mjv6rt7t15afc70k.apps.googleusercontent.com</string>
+    <string name="firebase_database_url" translatable="false">https://nextcloud-a7dea.firebaseio.com</string>
+    <string name="gcm_defaultSenderId" translatable="false">829118773643</string>
+    <string name="google_api_key" translatable="false">AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s</string>
+    <string name="google_app_id" translatable="false">1:829118773643:android:512449826e931d0e</string>
+    <string name="google_crash_reporting_api_key" translatable="false">AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s</string>
+    <string name="google_storage_bucket" translatable="false">nextcloud-a7dea.appspot.com</string>
+    <string name="project_id" translatable="false">nextcloud-a7dea</string>
 </resources>
 
 

+ 3 - 1
src/main/AndroidManifest.xml

@@ -268,13 +268,15 @@
         <activity android:name=".ui.activity.PassCodeActivity" />
         <activity android:name=".ui.activity.RequestCredentialsActivity" />
         <activity android:name=".ui.activity.ConflictsResolveActivity"/>
-        <activity android:name=".ui.activity.GenericExplanationActivity"/>
         <activity android:name=".ui.activity.ErrorsWhileCopyingHandlerActivity"/>
         
         <activity android:name=".ui.activity.LogHistoryActivity"/>
 
         <activity android:name=".ui.errorhandling.ErrorShowActivity" />
         <activity android:name=".ui.activity.UploadListActivity" />
+        <activity
+            android:name=".ui.trashbin.TrashbinActivity"
+            android:configChanges="orientation|screenSize|keyboardHidden"/>
         <activity android:name=".ui.activity.WhatsNewActivity"
                   android:theme="@style/Theme.ownCloud.noActionBar.Login" />
 

+ 32 - 6
src/main/java/com/owncloud/android/MainApp.java

@@ -35,6 +35,7 @@ import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.StrictMode;
+import android.support.annotation.RequiresApi;
 import android.support.annotation.StringRes;
 import android.support.multidex.MultiDexApplication;
 import android.support.v4.util.Pair;
@@ -43,6 +44,7 @@ import android.text.TextUtils;
 import android.view.WindowManager;
 
 import com.evernote.android.job.JobManager;
+import com.evernote.android.job.JobRequest;
 import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.authentication.PassCodeManager;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
@@ -55,10 +57,12 @@ import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.datastorage.DataStorageProvider;
 import com.owncloud.android.datastorage.StoragePoint;
 import com.owncloud.android.db.PreferenceManager;
+import com.owncloud.android.jobs.MediaFoldersDetectionJob;
 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.lib.resources.status.OwnCloudVersion;
 import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
 import com.owncloud.android.ui.activity.Preferences;
 import com.owncloud.android.ui.activity.SyncedFoldersActivity;
@@ -75,6 +79,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
@@ -89,6 +94,8 @@ import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFER
  */
 public class MainApp extends MultiDexApplication {
 
+    public static final OwnCloudVersion OUTDATED_SERVER_VERSION = OwnCloudVersion.nextcloud_12;
+
     private static final String TAG = MainApp.class.getSimpleName();
 
     private static final String AUTH_ON = "on";
@@ -160,6 +167,20 @@ public class MainApp extends MultiDexApplication {
         initContactsBackup();
         notificationChannels();
 
+
+        new JobRequest.Builder(MediaFoldersDetectionJob.TAG)
+                .setPeriodic(TimeUnit.MINUTES.toMillis(15), TimeUnit.MINUTES.toMillis(5))
+                .setUpdateCurrent(true)
+                .build()
+                .schedule();
+
+        new JobRequest.Builder(MediaFoldersDetectionJob.TAG)
+                .startNow()
+                .setUpdateCurrent(false)
+                .build()
+                .schedule();
+
+
         // register global protection with pass code
         registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
 
@@ -332,12 +353,17 @@ public class MainApp extends MultiDexApplication {
                 createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_PUSH,
                         R.string.notification_channel_push_name, R.string
                                 .notification_channel_push_description, context, NotificationManager.IMPORTANCE_DEFAULT);
+
+                createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_GENERAL, R.string
+                        .notification_channel_general_name, R.string.notification_channel_general_description,
+                        context, NotificationManager.IMPORTANCE_DEFAULT);
             } else {
                 Log_OC.e(TAG, "Notification manager is null");
             }
         }
     }
 
+    @RequiresApi(api = Build.VERSION_CODES.N)
     private static void createChannel(NotificationManager notificationManager,
                                       String channelId, int channelName,
                                       int channelDescription, Context context) {
@@ -541,8 +567,8 @@ public class MainApp extends MultiDexApplication {
 
             SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(contentResolver);
 
-            final List<MediaFolder> imageMediaFolders = MediaProvider.getImageFolders(contentResolver, 1, null);
-            final List<MediaFolder> videoMediaFolders = MediaProvider.getVideoFolders(contentResolver, 1, null);
+            final List<MediaFolder> imageMediaFolders = MediaProvider.getImageFolders(contentResolver, 1, null, true);
+            final List<MediaFolder> videoMediaFolders = MediaProvider.getVideoFolders(contentResolver, 1, null, true);
 
             ArrayList<Long> idsToDelete = new ArrayList<>();
             List<SyncedFolder> syncedFolders = syncedFolderProvider.getSyncedFolders();
@@ -553,8 +579,8 @@ public class MainApp extends MultiDexApplication {
                 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())) {
+                for (MediaFolder imageMediaFolder : imageMediaFolders) {
+                    if (imageMediaFolder.absolutePath.equals(syncedFolder.getLocalPath())) {
                         newSyncedFolder = (SyncedFolder) syncedFolder.clone();
                         newSyncedFolder.setType(MediaFolderType.IMAGE);
                         primaryKey = syncedFolderProvider.storeSyncedFolder(newSyncedFolder);
@@ -564,8 +590,8 @@ public class MainApp extends MultiDexApplication {
                     }
                 }
 
-                for (int j = 0; j < videoMediaFolders.size(); j++) {
-                    if (videoMediaFolders.get(j).absolutePath.equals(syncedFolder.getLocalPath())) {
+                for (MediaFolder videoMediaFolder : videoMediaFolders) {
+                    if (videoMediaFolder.absolutePath.equals(syncedFolder.getLocalPath())) {
                         newSyncedFolder = (SyncedFolder) syncedFolder.clone();
                         newSyncedFolder.setType(MediaFolderType.VIDEO);
                         primaryKey = syncedFolderProvider.storeSyncedFolder(newSyncedFolder);

+ 2 - 2
src/main/java/com/owncloud/android/authentication/AccountAuthenticatorActivity.java

@@ -34,8 +34,8 @@ import android.support.v7.app.AppCompatActivity;
 
 public class AccountAuthenticatorActivity extends AppCompatActivity {
 
-    private AccountAuthenticatorResponse mAccountAuthenticatorResponse = null;
-    private Bundle mResultBundle = null;
+    private AccountAuthenticatorResponse mAccountAuthenticatorResponse;
+    private Bundle mResultBundle;
 
 
     /**

+ 200 - 221
src/main/java/com/owncloud/android/authentication/AccountUtils.java

@@ -1,221 +1,200 @@
-/*
- *   ownCloud Android client application
- *
- *   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.authentication;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import com.owncloud.android.MainApp;
-import com.owncloud.android.datamodel.ArbitraryDataProvider;
-import com.owncloud.android.datamodel.FileDataStorageManager;
-import com.owncloud.android.lib.common.accounts.AccountUtils.Constants;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.lib.resources.status.OwnCloudVersion;
-import com.owncloud.android.operations.GetCapabilitiesOperarion;
-import com.owncloud.android.ui.activity.ManageAccountsActivity;
-
-
-public class AccountUtils {
-    private static final String TAG = AccountUtils.class.getSimpleName();
-    private static final String PREF_SELECT_OC_ACCOUNT = "select_oc_account";
-
-    public static final int ACCOUNT_VERSION = 1;
-    public static final int ACCOUNT_VERSION_WITH_PROPER_ID = 2;
-    public static final String ACCOUNT_USES_STANDARD_PASSWORD = "ACCOUNT_USES_STANDARD_PASSWORD";
-
-    private AccountUtils() {
-        // Required empty constructor
-    }
-
-    /**
-     * Can be used to get the currently selected ownCloud {@link Account} in the
-     * application preferences.
-     * 
-     * @param   context     The current application {@link Context}
-     * @return              The ownCloud {@link Account} currently saved in preferences, or the first 
-     *                      {@link Account} available, if valid (still registered in the system as ownCloud 
-     *                      account). If none is available and valid, returns null.
-     */
-    public static @Nullable Account getCurrentOwnCloudAccount(Context context) {
-        Account[] ocAccounts = getAccounts(context);
-        Account defaultAccount = null;
-
-        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(context.getContentResolver());
-
-        SharedPreferences appPreferences = PreferenceManager.getDefaultSharedPreferences(context);
-        String accountName = appPreferences.getString(PREF_SELECT_OC_ACCOUNT, null);
-
-        // account validation: the saved account MUST be in the list of ownCloud Accounts known by the AccountManager
-        if (accountName != null) {
-            for (Account account : ocAccounts) {
-                if (account.name.equals(accountName)) {
-                    defaultAccount = account;
-                    break;
-                }
-            }
-        }
-
-        if (defaultAccount == null && ocAccounts.length > 0) {
-            // take first which is not pending for removal account as fallback
-            for (Account account: ocAccounts) {
-                boolean pendingForRemoval = arbitraryDataProvider.getBooleanValue(account,
-                        ManageAccountsActivity.PENDING_FOR_REMOVAL);
-
-                if (!pendingForRemoval) {
-                    defaultAccount = account;
-                    break;
-                }
-            }
-        }
-
-        return defaultAccount;
-    }
-
-    public static Account[] getAccounts(Context context) {
-        AccountManager accountManager = AccountManager.get(context);
-        return accountManager.getAccountsByType(MainApp.getAccountType(context));
-    }
-
-    
-    public static boolean exists(Account account, Context context) {
-        Account[] ocAccounts = getAccounts(context);
-
-        if (account != null && account.name != null) {
-            int lastAtPos = account.name.lastIndexOf('@');
-            String hostAndPort = account.name.substring(lastAtPos + 1);
-            String username = account.name.substring(0, lastAtPos);
-            String otherHostAndPort;
-            String otherUsername;
-            for (Account otherAccount : ocAccounts) {
-                lastAtPos = otherAccount.name.lastIndexOf('@');
-                otherHostAndPort = otherAccount.name.substring(lastAtPos + 1);
-                otherUsername = otherAccount.name.substring(0, lastAtPos);
-                if (otherHostAndPort.equals(hostAndPort) &&
-                        otherUsername.equalsIgnoreCase(username)) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    /**
-     * returns the user's name based on the account name.
-     *
-     * @param accountName the account name
-     * @return the user's name
-     */
-    public static String getAccountUsername(String accountName) {
-        if (accountName != null) {
-            return accountName.substring(0, accountName.lastIndexOf('@'));
-        } else {
-            return null;
-        }
-    }
-    
-    /**
-     * Returns owncloud account identified by accountName or null if it does not exist.
-     * @param context the context
-     * @param accountName name of account to be returned
-     * @return owncloud account named accountName
-     */
-    public static Account getOwnCloudAccountByName(Context context, String accountName) {
-        Account[] ocAccounts = AccountManager.get(context).getAccountsByType(MainApp.getAccountType(context));
-        for (Account account : ocAccounts) {
-            if(account.name.equals(accountName)) {
-                return account;
-            }
-        }
-        return null;
-    }
-
-
-    public static boolean setCurrentOwnCloudAccount(final Context context, String accountName) {
-        boolean result = false;
-        if (accountName != null) {
-            boolean found;
-            for (final Account account : getAccounts(context)) {
-                found = (account.name.equals(accountName));
-                if (found) {
-                    SharedPreferences.Editor appPrefs = PreferenceManager.getDefaultSharedPreferences(context).edit();
-                    appPrefs.putString(PREF_SELECT_OC_ACCOUNT, accountName);
-
-                    // update credentials
-                    Thread t = new Thread(() -> {
-                        FileDataStorageManager storageManager = new FileDataStorageManager(account,
-                                context.getContentResolver());
-                        GetCapabilitiesOperarion getCapabilities = new GetCapabilitiesOperarion();
-                        RemoteOperationResult updateResult = getCapabilities.execute(storageManager, context);
-                        Log_OC.w(TAG, "Update Capabilities: " + updateResult.isSuccess());
-                    });
-
-                    t.start();
-
-                    appPrefs.apply();
-                    result = true;
-                    break;
-                }
-            }
-        }
-        return result;
-    }
-
-    public static void resetOwnCloudAccount(Context context) {
-        SharedPreferences.Editor appPrefs = PreferenceManager.getDefaultSharedPreferences(context).edit();
-        appPrefs.putString(PREF_SELECT_OC_ACCOUNT, null);
-
-        appPrefs.apply();
-    }
-
-    /**
-     * Access the version of the OC server corresponding to an account SAVED IN THE ACCOUNTMANAGER
-     *
-     * @param   account     ownCloud account
-     * @return              Version of the OC server corresponding to account, according to the data saved
-     *                      in the system AccountManager
-     */
-    public static @NonNull
-    OwnCloudVersion getServerVersion(Account account) {
-        OwnCloudVersion serverVersion = OwnCloudVersion.nextcloud_10;
-        if (account != null) {
-            AccountManager accountMgr = AccountManager.get(MainApp.getAppContext());
-            String serverVersionStr = accountMgr.getUserData(account, Constants.KEY_OC_VERSION);
-            if (serverVersionStr != null) {
-                serverVersion = new OwnCloudVersion(serverVersionStr);
-            }
-        }
-        return serverVersion;
-    }
-
-    public static boolean hasSearchUsersSupport(Account account) {
-        return getServerVersion(account).isSearchUsersSupported();
-    }
-
-    public static boolean hasSearchSupport(Account account) {
-        return getServerVersion(account).isSearchSupported();
-    }
-}
+/*
+ *   ownCloud Android client application
+ *
+ *   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.authentication;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.owncloud.android.MainApp;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
+import com.owncloud.android.lib.common.accounts.AccountUtils.Constants;
+import com.owncloud.android.lib.resources.status.OwnCloudVersion;
+import com.owncloud.android.ui.activity.ManageAccountsActivity;
+
+
+public class AccountUtils {
+    private static final String PREF_SELECT_OC_ACCOUNT = "select_oc_account";
+
+    public static final int ACCOUNT_VERSION = 1;
+    public static final int ACCOUNT_VERSION_WITH_PROPER_ID = 2;
+    public static final String ACCOUNT_USES_STANDARD_PASSWORD = "ACCOUNT_USES_STANDARD_PASSWORD";
+
+    private AccountUtils() {
+        // Required empty constructor
+    }
+
+    /**
+     * Can be used to get the currently selected ownCloud {@link Account} in the
+     * application preferences.
+     * 
+     * @param   context     The current application {@link Context}
+     * @return              The ownCloud {@link Account} currently saved in preferences, or the first 
+     *                      {@link Account} available, if valid (still registered in the system as ownCloud 
+     *                      account). If none is available and valid, returns null.
+     */
+    public static @Nullable Account getCurrentOwnCloudAccount(Context context) {
+        Account[] ocAccounts = getAccounts(context);
+        Account defaultAccount = null;
+
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(context.getContentResolver());
+
+        SharedPreferences appPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+        String accountName = appPreferences.getString(PREF_SELECT_OC_ACCOUNT, null);
+
+        // account validation: the saved account MUST be in the list of ownCloud Accounts known by the AccountManager
+        if (accountName != null) {
+            for (Account account : ocAccounts) {
+                if (account.name.equals(accountName)) {
+                    defaultAccount = account;
+                    break;
+                }
+            }
+        }
+
+        if (defaultAccount == null && ocAccounts.length > 0) {
+            // take first which is not pending for removal account as fallback
+            for (Account account: ocAccounts) {
+                boolean pendingForRemoval = arbitraryDataProvider.getBooleanValue(account,
+                        ManageAccountsActivity.PENDING_FOR_REMOVAL);
+
+                if (!pendingForRemoval) {
+                    defaultAccount = account;
+                    break;
+                }
+            }
+        }
+
+        return defaultAccount;
+    }
+
+    public static Account[] getAccounts(Context context) {
+        AccountManager accountManager = AccountManager.get(context);
+        return accountManager.getAccountsByType(MainApp.getAccountType(context));
+    }
+
+    
+    public static boolean exists(Account account, Context context) {
+        Account[] ocAccounts = getAccounts(context);
+
+        if (account != null && account.name != null) {
+            int lastAtPos = account.name.lastIndexOf('@');
+            String hostAndPort = account.name.substring(lastAtPos + 1);
+            String username = account.name.substring(0, lastAtPos);
+            String otherHostAndPort;
+            String otherUsername;
+            for (Account otherAccount : ocAccounts) {
+                lastAtPos = otherAccount.name.lastIndexOf('@');
+                otherHostAndPort = otherAccount.name.substring(lastAtPos + 1);
+                otherUsername = otherAccount.name.substring(0, lastAtPos);
+                if (otherHostAndPort.equals(hostAndPort) &&
+                        otherUsername.equalsIgnoreCase(username)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * returns the user's name based on the account name.
+     *
+     * @param accountName the account name
+     * @return the user's name
+     */
+    public static String getAccountUsername(String accountName) {
+        if (accountName != null) {
+            return accountName.substring(0, accountName.lastIndexOf('@'));
+        } else {
+            return null;
+        }
+    }
+    
+    /**
+     * Returns owncloud account identified by accountName or null if it does not exist.
+     * @param context the context
+     * @param accountName name of account to be returned
+     * @return owncloud account named accountName
+     */
+    public static Account getOwnCloudAccountByName(Context context, String accountName) {
+        Account[] ocAccounts = AccountManager.get(context).getAccountsByType(MainApp.getAccountType(context));
+        for (Account account : ocAccounts) {
+            if(account.name.equals(accountName)) {
+                return account;
+            }
+        }
+        return null;
+    }
+
+
+    public static boolean setCurrentOwnCloudAccount(final Context context, String accountName) {
+        boolean result = false;
+        if (accountName != null) {
+            boolean found;
+            for (final Account account : getAccounts(context)) {
+                found = (account.name.equals(accountName));
+                if (found) {
+                    SharedPreferences.Editor appPrefs = PreferenceManager.getDefaultSharedPreferences(context).edit();
+                    appPrefs.putString(PREF_SELECT_OC_ACCOUNT, accountName);
+                    appPrefs.apply();
+                    result = true;
+                    break;
+                }
+            }
+        }
+        return result;
+    }
+
+    public static void resetOwnCloudAccount(Context context) {
+        SharedPreferences.Editor appPrefs = PreferenceManager.getDefaultSharedPreferences(context).edit();
+        appPrefs.putString(PREF_SELECT_OC_ACCOUNT, null);
+
+        appPrefs.apply();
+    }
+
+    /**
+     * Access the version of the OC server corresponding to an account SAVED IN THE ACCOUNTMANAGER
+     *
+     * @param   account     ownCloud account
+     * @return              Version of the OC server corresponding to account, according to the data saved
+     *                      in the system AccountManager
+     */
+    public static @NonNull
+    OwnCloudVersion getServerVersion(Account account) {
+        OwnCloudVersion serverVersion = OwnCloudVersion.nextcloud_10;
+        if (account != null) {
+            AccountManager accountMgr = AccountManager.get(MainApp.getAppContext());
+            String serverVersionStr = accountMgr.getUserData(account, Constants.KEY_OC_VERSION);
+            if (serverVersionStr != null) {
+                serverVersion = new OwnCloudVersion(serverVersionStr);
+            }
+        }
+        return serverVersion;
+    }
+
+    public static boolean hasSearchSupport(Account account) {
+        return getServerVersion(account).isSearchSupported();
+    }
+}

+ 56 - 46
src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java

@@ -206,11 +206,11 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
     private TextView mServerStatusView;
 
     private TextWatcher mHostUrlInputWatcher;
-    private String mServerStatusText;
-    private int mServerStatusIcon = 0;
+    private String mServerStatusText = "";
+    private int mServerStatusIcon;
 
-    private boolean mServerIsChecked = false;
-    private boolean mServerIsValid = false;
+    private boolean mServerIsChecked;
+    private boolean mServerIsValid;
 
     private GetServerInfoOperation.ServerInfo mServerInfo = new GetServerInfoOperation.ServerInfo();
 
@@ -226,8 +226,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
 
     private WebView mLoginWebView;
 
-    private String mAuthStatusText;
-    private int mAuthStatusIcon = 0;
+    private String mAuthStatusText = "";
+    private int mAuthStatusIcon;
 
     private String mAuthToken = "";
     private AuthenticatorAsyncTask mAsyncTask;
@@ -246,7 +246,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
     private String webViewPassword;
     private TextInputLayout mUsernameInputLayout;
     private TextInputLayout mPasswordInputLayout;
-    private boolean forceOldLoginMethod = false;
+    private boolean forceOldLoginMethod;
 
     /**
      * {@inheritDoc}
@@ -379,6 +379,47 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
 
         mLoginWebView.loadUrl(url, headers);
 
+        setClient(progressBar);
+
+        // show snackbar after 60s to switch back to old login method
+        new Handler().postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                DisplayUtils.createSnackbar(mLoginWebView, R.string.fallback_weblogin_text, Snackbar.LENGTH_INDEFINITE)
+                        .setActionTextColor(getResources().getColor(R.color.primary_dark))
+                        .setAction(R.string.fallback_weblogin_back, new View.OnClickListener() {
+                            @Override
+                            public void onClick(View v) {
+                                mLoginWebView.setVisibility(View.INVISIBLE);
+                                webViewLoginMethod = false;
+
+                                setContentView(R.layout.account_setup);
+
+                                // initialize general UI elements
+                                initOverallUi();
+
+                                mPasswordInputLayout.setVisibility(View.VISIBLE);
+                                mUsernameInputLayout.setVisibility(View.VISIBLE);
+                                mUsernameInput.requestFocus();
+                                mOAuth2Check.setVisibility(View.INVISIBLE);
+                                mAuthStatusView.setVisibility(View.INVISIBLE);
+                                mServerStatusView.setVisibility(View.INVISIBLE);
+                                mTestServerButton.setVisibility(View.INVISIBLE);
+                                forceOldLoginMethod = true;
+                                mOkButton.setVisibility(View.VISIBLE);
+
+                                initServerPreFragment(null);
+
+                                mHostUrlInput.setText(baseURL);
+
+                                checkOcServer();
+                            }
+                        }).show();
+            }
+        }, 60000);
+    }
+
+    private void setClient(ProgressBar progressBar) {
         mLoginWebView.setWebViewClient(new WebViewClient() {
             @Override
             public boolean shouldOverrideUrlLoading(WebView view, String url) {
@@ -424,43 +465,6 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
                 }
             }
         });
-
-        // show snackbar after 60s to switch back to old login method
-        new Handler().postDelayed(new Runnable() {
-            @Override
-            public void run() {
-                DisplayUtils.createSnackbar(mLoginWebView, R.string.fallback_weblogin_text, Snackbar.LENGTH_INDEFINITE)
-                        .setActionTextColor(getResources().getColor(R.color.primary_dark))
-                        .setAction(R.string.fallback_weblogin_back, new View.OnClickListener() {
-                            @Override
-                            public void onClick(View v) {
-                                mLoginWebView.setVisibility(View.INVISIBLE);
-                                webViewLoginMethod = false;
-
-                                setContentView(R.layout.account_setup);
-
-                                // initialize general UI elements
-                                initOverallUi();
-
-                                mPasswordInputLayout.setVisibility(View.VISIBLE);
-                                mUsernameInputLayout.setVisibility(View.VISIBLE);
-                                mUsernameInput.requestFocus();
-                                mOAuth2Check.setVisibility(View.INVISIBLE);
-                                mAuthStatusView.setVisibility(View.INVISIBLE);
-                                mServerStatusView.setVisibility(View.INVISIBLE);
-                                mTestServerButton.setVisibility(View.INVISIBLE);
-                                forceOldLoginMethod = true;
-                                mOkButton.setVisibility(View.VISIBLE);
-
-                                initServerPreFragment(null);
-
-                                mHostUrlInput.setText(baseURL);
-
-                                checkOcServer();
-                            }
-                        }).show();
-            }
-        }, 60000);
     }
 
     private void parseAndLoginFromWebView(String dataString) {
@@ -541,7 +545,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         mAuthTokenType = getIntent().getExtras().getString(AccountAuthenticator.KEY_AUTH_TOKEN_TYPE);
         if (mAuthTokenType == null) {
             if (mAccount != null) {
-                boolean oAuthRequired = (mAccountMgr.getUserData(mAccount, Constants.KEY_SUPPORTS_OAUTH2) != null);
+                boolean oAuthRequired = mAccountMgr.getUserData(mAccount, Constants.KEY_SUPPORTS_OAUTH2) != null;
                 boolean samlWebSsoRequired = (
                         mAccountMgr.getUserData
                                 (mAccount, Constants.KEY_SUPPORTS_SAML_WEB_SSO) != null
@@ -1418,6 +1422,12 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
             //      4. we got the authentication method required by the server 
             mServerInfo = (GetServerInfoOperation.ServerInfo) (result.getData().get(0));
 
+            // show outdated warning
+            if (getResources().getBoolean(R.bool.show_outdated_server_warning) &&
+                    mServerInfo.mVersion.compareTo(MainApp.OUTDATED_SERVER_VERSION) < 0) {
+                DisplayUtils.showServerOutdatedSnackbar(this);
+            }
+
             webViewLoginMethod = mServerInfo.mVersion.isWebLoginSupported() && !forceOldLoginMethod;
 
             if (webViewUser != null && !webViewUser.isEmpty() &&
@@ -1957,7 +1967,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
      * to the last check on the ownCloud server.
      */
     private void showServerStatus() {
-        if (mServerStatusIcon == 0 && "".equals(mServerStatusText) || forceOldLoginMethod) {
+        if (mServerStatusIcon == 0 && "".equals(mServerStatusText)) {
             mServerStatusView.setVisibility(View.INVISIBLE);
         } else {
             mServerStatusView.setText(mServerStatusText);

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

@@ -91,7 +91,7 @@ public abstract class AuthenticatorUrlUtils {
 
             normalizedUrl = normalizeUrlSuffix(normalizedUrl);
         }
-        return (normalizedUrl != null ? normalizedUrl : "");
+        return normalizedUrl != null ? normalizedUrl : "";
     }
 
     public static String trimWebdavSuffix(String url) {

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

@@ -29,12 +29,11 @@ import android.net.Uri;
 import com.owncloud.android.db.ProviderMeta;
 import com.owncloud.android.lib.common.utils.Log_OC;
 
-import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Database provider for handling the persistence aspects of arbitrary data table.
  */
-
 public class ArbitraryDataProvider {
     static private final String TAG = ArbitraryDataProvider.class.getSimpleName();
 
@@ -56,7 +55,7 @@ public class ArbitraryDataProvider {
         );
     }
 
-    public int deleteForKeyWhereAccountNotIn(ArrayList<String> accounts, String key) {
+    public int deleteForKeyWhereAccountNotIn(List<String> accounts, String key) {
         return contentResolver.delete(
                 ProviderMeta.ProviderTableMeta.CONTENT_URI_ARBITRARY_DATA,
                 ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID + " NOT IN (?) AND " +
@@ -178,6 +177,7 @@ public class ArbitraryDataProvider {
                 if (value == null) {
                     Log_OC.e(TAG, "Arbitrary value could not be created from cursor");
                 } else {
+                    cursor.close();
                     return value;
                 }
             }

+ 13 - 13
src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadata.java

@@ -22,14 +22,14 @@
 package com.owncloud.android.datamodel;
 
 import java.util.HashMap;
+import java.util.Map;
 
 /**
  * Decrypted class representation of metadata json of folder metadata
  */
-
 public class DecryptedFolderMetadata {
     private Metadata metadata;
-    private HashMap<String, DecryptedFile> files;
+    private Map<String, DecryptedFile> files;
 
     public DecryptedFolderMetadata() {
         this.metadata = new Metadata();
@@ -49,24 +49,24 @@ public class DecryptedFolderMetadata {
         this.metadata = metadata;
     }
 
-    public HashMap<String, DecryptedFile> getFiles() {
+    public Map<String, DecryptedFile> getFiles() {
         return files;
     }
 
-    public void setFiles(HashMap<String, DecryptedFile> files) {
+    public void setFiles(Map<String, DecryptedFile> files) {
         this.files = files;
     }
 
     public static class Metadata {
-        private HashMap<Integer, String> metadataKeys; // each keys is encrypted on its own, decrypt on use
+        private Map<Integer, String> metadataKeys; // each keys is encrypted on its own, decrypt on use
         private Sharing sharing;
         private int version;
 
-        public HashMap<Integer, String> getMetadataKeys() {
+        public Map<Integer, String> getMetadataKeys() {
             return metadataKeys;
         }
 
-        public void setMetadataKeys(HashMap<Integer, String> metadataKeys) {
+        public void setMetadataKeys(Map<Integer, String> metadataKeys) {
             this.metadataKeys = metadataKeys;
         }
 
@@ -93,26 +93,26 @@ public class DecryptedFolderMetadata {
     }
 
     public static class Encrypted {
-        private HashMap<Integer, String> metadataKeys;
+        private Map<Integer, String> metadataKeys;
 
-        public HashMap<Integer, String> getMetadataKeys() {
+        public Map<Integer, String> getMetadataKeys() {
             return metadataKeys;
         }
 
-        public void setMetadataKeys(HashMap<Integer, String> metadataKeys) {
+        public void setMetadataKeys(Map<Integer, String> metadataKeys) {
             this.metadataKeys = metadataKeys;
         }
     }
 
     public static class Sharing {
-        private HashMap<String, String> recipient;
+        private Map<String, String> recipient;
         private String signature;
 
-        public HashMap<String, String> getRecipient() {
+        public Map<String, String> getRecipient() {
             return recipient;
         }
 
-        public void setRecipient(HashMap<String, String> recipient) {
+        public void setRecipient(Map<String, String> recipient) {
             this.recipient = recipient;
         }
 

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

@@ -21,7 +21,7 @@
 
 package com.owncloud.android.datamodel;
 
-import java.util.HashMap;
+import java.util.Map;
 
 /**
  * Encrypted class representation of metadata json of folder metadata
@@ -29,9 +29,9 @@ import java.util.HashMap;
 
 public class EncryptedFolderMetadata {
     private DecryptedFolderMetadata.Metadata metadata;
-    private HashMap<String, EncryptedFile> files;
+    private Map<String, EncryptedFile> files;
     
-    public EncryptedFolderMetadata(DecryptedFolderMetadata.Metadata metadata, HashMap<String, EncryptedFile> files) {
+    public EncryptedFolderMetadata(DecryptedFolderMetadata.Metadata metadata, Map<String, EncryptedFile> files) {
         this.metadata = metadata;
         this.files = files;
     }
@@ -44,11 +44,11 @@ public class EncryptedFolderMetadata {
         this.metadata = metadata;
     }
 
-    public HashMap<String, EncryptedFile> getFiles() {
+    public Map<String, EncryptedFile> getFiles() {
         return files;
     }
 
-    public void setFiles(HashMap<String, EncryptedFile> files) {
+    public void setFiles(Map<String, EncryptedFile> files) {
         this.files = files;
     }
 
@@ -91,4 +91,3 @@ public class EncryptedFolderMetadata {
         }
     }
 }
-

+ 3 - 2
src/main/java/com/owncloud/android/datamodel/ExternalLinksProvider.java

@@ -31,6 +31,7 @@ import com.owncloud.android.lib.common.ExternalLinkType;
 import com.owncloud.android.lib.common.utils.Log_OC;
 
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Database provider for handling the persistence aspects of {@link com.owncloud.android.lib.common.ExternalLink}s.
@@ -82,7 +83,7 @@ public class ExternalLinksProvider {
      *
      * @return external links, empty if none exists
      */
-    public ArrayList<ExternalLink> getExternalLink(ExternalLinkType type) {
+    public List<ExternalLink> getExternalLink(ExternalLinkType type) {
         Cursor cursor = mContentResolver.query(
                 ProviderMeta.ProviderTableMeta.CONTENT_URI_EXTERNAL_LINKS,
                 null,
@@ -92,7 +93,7 @@ public class ExternalLinksProvider {
         );
 
         if (cursor != null) {
-            ArrayList<ExternalLink> list = new ArrayList<>();
+            List<ExternalLink> list = new ArrayList<>();
             if (cursor.moveToFirst()) {
                 do {
                     ExternalLink externalLink = createExternalLinkFromCursor(cursor);

+ 17 - 6
src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -141,6 +141,17 @@ public class FileDataStorageManager {
         return file;
     }
 
+    public @Nullable
+    OCFile getFileByRemoteId(String remoteId) {
+        Cursor c = getFileCursorForValue(ProviderTableMeta.FILE_REMOTE_ID, remoteId);
+        OCFile file = null;
+        if (c.moveToFirst()) {
+            file = createFileInstance(c);
+        }
+        c.close();
+        return file;
+    }
+
     public boolean fileExists(long id) {
         return fileExists(ProviderTableMeta._ID, String.valueOf(id));
     }
@@ -185,7 +196,7 @@ public class FileDataStorageManager {
         );
         cv.put(ProviderTableMeta.FILE_CREATION, file.getCreationTimestamp());
         cv.put(ProviderTableMeta.FILE_CONTENT_LENGTH, file.getFileLength());
-        cv.put(ProviderTableMeta.FILE_CONTENT_TYPE, file.getMimetype());
+        cv.put(ProviderTableMeta.FILE_CONTENT_TYPE, file.getMimeType());
         cv.put(ProviderTableMeta.FILE_NAME, file.getFileName());
         cv.put(ProviderTableMeta.FILE_ENCRYPTED_NAME, file.getEncryptedFileName());
         cv.put(ProviderTableMeta.FILE_PARENT, file.getParentId());
@@ -430,7 +441,7 @@ public class FileDataStorageManager {
         );
         cv.put(ProviderTableMeta.FILE_CREATION, folder.getCreationTimestamp());
         cv.put(ProviderTableMeta.FILE_CONTENT_LENGTH, 0);
-        cv.put(ProviderTableMeta.FILE_CONTENT_TYPE, folder.getMimetype());
+        cv.put(ProviderTableMeta.FILE_CONTENT_TYPE, folder.getMimeType());
         cv.put(ProviderTableMeta.FILE_NAME, folder.getFileName());
         cv.put(ProviderTableMeta.FILE_PARENT, folder.getParentId());
         cv.put(ProviderTableMeta.FILE_PATH, folder.getRemotePath());
@@ -455,7 +466,7 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, file.getModificationTimestampAtLastSyncForData());
         cv.put(ProviderTableMeta.FILE_CREATION, file.getCreationTimestamp());
         cv.put(ProviderTableMeta.FILE_CONTENT_LENGTH, file.getFileLength());
-        cv.put(ProviderTableMeta.FILE_CONTENT_TYPE, file.getMimetype());
+        cv.put(ProviderTableMeta.FILE_CONTENT_TYPE, file.getMimeType());
         cv.put(ProviderTableMeta.FILE_NAME, file.getFileName());
         cv.put(ProviderTableMeta.FILE_ENCRYPTED_NAME, file.getEncryptedFileName());
         cv.put(ProviderTableMeta.FILE_PARENT, folder.getFileId());
@@ -1367,7 +1378,7 @@ public class FileDataStorageManager {
                 );
                 cv.put(ProviderTableMeta.FILE_CREATION, file.getCreationTimestamp());
                 cv.put(ProviderTableMeta.FILE_CONTENT_LENGTH, file.getFileLength());
-                cv.put(ProviderTableMeta.FILE_CONTENT_TYPE, file.getMimetype());
+                cv.put(ProviderTableMeta.FILE_CONTENT_TYPE, file.getMimeType());
                 cv.put(ProviderTableMeta.FILE_NAME, file.getFileName());
                 cv.put(ProviderTableMeta.FILE_PARENT, file.getParentId());
                 cv.put(ProviderTableMeta.FILE_PATH, file.getRemotePath());
@@ -1645,7 +1656,7 @@ public class FileDataStorageManager {
 
     }
 
-    public ArrayList<OCShare> getSharesWithForAFile(String filePath, String accountName) {
+    public List<OCShare> getSharesWithForAFile(String filePath, String accountName) {
         // Condition
         String where = ProviderTableMeta.OCSHARES_PATH + AND
                 + ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + AND
@@ -2182,7 +2193,7 @@ public class FileDataStorageManager {
         if (onlyImages) {
             List<OCFile> temp = new ArrayList<>();
 
-            for (OCFile file : temp) {
+            for (OCFile file : ocFiles) {
                 if (MimeTypeUtil.isImage(file)) {
                     temp.add(file);
                 }

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

@@ -29,7 +29,6 @@ import com.owncloud.android.lib.common.utils.Log_OC;
 
 import java.io.BufferedInputStream;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.HashSet;
@@ -222,8 +221,6 @@ public class FilesystemDataProvider {
 
             return crc.getValue();
 
-        } catch (FileNotFoundException e) {
-            return -1;
         } catch (IOException e) {
             return -1;
         }

+ 56 - 0
src/main/java/com/owncloud/android/datamodel/MediaFoldersModel.java

@@ -0,0 +1,56 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2018 Mario Danic
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datamodel;
+
+import java.util.List;
+
+public class MediaFoldersModel {
+    private List<String> imageMediaFolders;
+    private List<String> videoMediaFolders;
+
+    /**
+     * default constructor.
+     */
+    public MediaFoldersModel() {
+        // keep default constructor for GSON
+    }
+
+    public MediaFoldersModel(List<String> imageMediaFolders, List<String> videoMediaFolders) {
+        this.imageMediaFolders = imageMediaFolders;
+        this.videoMediaFolders = videoMediaFolders;
+    }
+
+    public List<String> getImageMediaFolders() {
+        return imageMediaFolders;
+    }
+
+    public void setImageMediaFolders(List<String> imageMediaFolders) {
+        this.imageMediaFolders = imageMediaFolders;
+    }
+
+    public List<String> getVideoMediaFolders() {
+        return videoMediaFolders;
+    }
+
+    public void setVideoMediaFolders(List<String> videoMediaFolders) {
+        this.videoMediaFolders = videoMediaFolders;
+    }
+}

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

@@ -66,14 +66,14 @@ public class MediaProvider {
      * @return list with media folders
      */
     public static List<MediaFolder> getImageFolders(ContentResolver contentResolver, int itemLimit,
-                                                    @Nullable final Activity activity) {
+                                                    @Nullable final Activity activity, boolean getWithoutActivity) {
         // check permissions
         checkPermissions(activity);
 
         // query media/image folders
         Cursor cursorFolders = null;
-        if (activity != null && PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
-                Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+        if ((activity != null && PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
+                Manifest.permission.WRITE_EXTERNAL_STORAGE)) || getWithoutActivity) {
             cursorFolders = contentResolver.query(IMAGES_MEDIA_URI, IMAGES_FOLDER_PROJECTION, null, null,
                     IMAGES_FOLDER_SORT_ORDER);
         }
@@ -171,14 +171,14 @@ public class MediaProvider {
     }
 
     public static List<MediaFolder> getVideoFolders(ContentResolver contentResolver, int itemLimit,
-                                                    @Nullable final Activity activity) {
+                                                    @Nullable final Activity activity, boolean getWithoutActivity) {
         // check permissions
         checkPermissions(activity);
 
         // query media/image folders
         Cursor cursorFolders = null;
-        if (activity != null && PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
-                Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+        if ((activity != null && PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
+                Manifest.permission.WRITE_EXTERNAL_STORAGE)) || getWithoutActivity) {
             cursorFolders = contentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, VIDEOS_FOLDER_PROJECTION,
                     null, null, null);
         } 

+ 13 - 15
src/main/java/com/owncloud/android/datamodel/OCFile.java

@@ -35,13 +35,14 @@ import com.owncloud.android.R;
 import com.owncloud.android.lib.common.network.WebdavEntry;
 import com.owncloud.android.lib.common.network.WebdavUtils;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.lib.resources.files.ServerFileInterface;
 import com.owncloud.android.utils.MimeType;
 
 import java.io.File;
 
 import third_parties.daveKoeller.AlphanumComparator;
 
-public class OCFile implements Parcelable, Comparable<OCFile> {
+public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterface {
 
     public static final Parcelable.Creator<OCFile> CREATOR = new Parcelable.Creator<OCFile>() {
 
@@ -302,8 +303,7 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
      */
     public boolean existsOnDevice() {
         if (mLocalPath != null && mLocalPath.length() > 0) {
-            File file = new File(mLocalPath);
-            return (file.exists());
+            return new File(mLocalPath).exists();
         }
         return false;
     }
@@ -466,7 +466,7 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
         Log_OC.d(TAG, "OCFile name changing from " + mRemotePath);
         if (name != null && name.length() > 0 && !name.contains(PATH_SEPARATOR) &&
                 !mRemotePath.equals(ROOT_PATH)) {
-            String parent = (new File(getRemotePath())).getParent();
+            String parent = (new File(this.getRemotePath())).getParent();
             parent = (parent.endsWith(PATH_SEPARATOR)) ? parent : parent + PATH_SEPARATOR;
             mRemotePath = parent + name;
             if (isFolder()) {
@@ -485,11 +485,12 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
     }
 
     /**
-     * Can be used to get the Mimetype
+     * Can be used to get the MimeType
      *
-     * @return the Mimetype as a String
+     * @return the MimeType as a String
      */
-    public String getMimetype() {
+    @Override
+    public String getMimeType() {
         return mMimeType;
     }
 
@@ -585,8 +586,8 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
      * @return remote path
      */
     public String getParentRemotePath() {
-        String parentPath = new File(getRemotePath()).getParent();
-        return (parentPath.endsWith("/")) ? parentPath : (parentPath + "/");
+        String parentPath = new File(this.getRemotePath()).getParent();
+        return parentPath.endsWith("/") ? parentPath : parentPath + "/";
     }
 
     /**
@@ -663,19 +664,16 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
 
     @Override
     public int hashCode() {
-        int result = (int) (mId ^ (mId >>> 32));
-        result = 31 * result + (int) (mParentId ^ (mParentId >>> 32));
-        return result;
+        return 31 * (int) (mId ^ (mId >>> 32)) + (int) (mParentId ^ (mParentId >>> 32));
     }
 
     @Override
     public String toString() {
         String asString = "[id=%s, name=%s, mime=%s, downloaded=%s, local=%s, remote=%s, " +
                 "parentId=%s, availableOffline=%s etag=%s favourite=%s]";
-        asString = String.format(asString, mId, getFileName(), mMimeType, isDown(),
+        return String.format(asString, mId, getFileName(), mMimeType, isDown(),
                 mLocalPath, mRemotePath, mParentId, mAvailableOffline,
                 mEtag, mIsFavorite);
-        return asString;
     }
 
     public String getEtag() {
@@ -778,7 +776,7 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
 
     public boolean isSharedWithMe() {
         String permissions = getPermissions();
-        return (permissions != null && permissions.contains(PERMISSION_SHARED_WITH_ME));
+        return permissions != null && permissions.contains(PERMISSION_SHARED_WITH_ME);
     }
 
     public boolean canReshare() {

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

@@ -78,6 +78,24 @@ public class SyncedFolderProvider extends Observable {
         }
     }
 
+    public int countEnabledSyncedFolders() {
+        int count = 0;
+        Cursor cursor = mContentResolver.query(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
+                null,
+                ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED + " = ?",
+                new String[]{"1"},
+                null
+        );
+
+        if (cursor != null) {
+             count = cursor.getCount();
+             cursor.close();
+        }
+
+        return count;
+    }
+
     /**
      * get all synced folder entries.
      *
@@ -160,6 +178,37 @@ public class SyncedFolderProvider extends Observable {
         return result;
     }
 
+    public SyncedFolder findByLocalPathAndAccount(String localPath, Account account) {
+
+        SyncedFolder result = null;
+        Cursor cursor = mContentResolver.query(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
+                null,
+                ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH + " == \"" + localPath + "\"" + " AND " +
+                ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT + " == " + account.name,
+                null,
+                null
+        );
+
+        if (cursor != null && cursor.getCount() == 1) {
+            result = createSyncedFolderFromCursor(cursor);
+        } else {
+            if (cursor == null) {
+                Log_OC.e(TAG, "Sync folder db cursor for local path=" + localPath + " in NULL.");
+            } else {
+                Log_OC.e(TAG, cursor.getCount() + " items for local path=" + localPath
+                        + " available in sync folder db. Expected 1. Failed to update sync folder db.");
+            }
+        }
+
+        if (cursor != null) {
+            cursor.close();
+        }
+
+        return result;
+
+    }
+
     /**
      * find a synced folder by local path.
      *
@@ -171,7 +220,7 @@ public class SyncedFolderProvider extends Observable {
         Cursor cursor = mContentResolver.query(
                 ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
                 null,
-                ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH + "== \"" + localPath + "\"",
+                ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH + " == \"" + localPath + "\"",
                 null,
                 null
         );
@@ -232,8 +281,7 @@ public class SyncedFolderProvider extends Observable {
      */
     public void updateAutoUploadPaths(Context context) {
         List<SyncedFolder> syncedFolders = getSyncedFolders();
-        for (int i = 0; i < syncedFolders.size(); i++) {
-            SyncedFolder syncedFolder = syncedFolders.get(i);
+        for (SyncedFolder syncedFolder : syncedFolders) {
             if (!new File(syncedFolder.getLocalPath()).exists()) {
                 String localPath = syncedFolder.getLocalPath();
                 if (localPath.endsWith("/")) {
@@ -241,7 +289,7 @@ public class SyncedFolderProvider extends Observable {
                 }
                 localPath = localPath.substring(0, localPath.lastIndexOf('/'));
                 if (new File(localPath).exists()) {
-                    syncedFolders.get(i).setLocalPath(localPath);
+                    syncedFolder.setLocalPath(localPath);
                     updateSyncFolder(syncedFolder);
                 } else {
                     deleteSyncFolderWithId(syncedFolder.getId());
@@ -261,7 +309,7 @@ public class SyncedFolderProvider extends Observable {
      * @param ids     the list of ids to be excluded from deletion.
      * @return number of deleted records.
      */
-    public int deleteSyncedFoldersNotInList(Context context, ArrayList<Long> ids) {
+    public int deleteSyncedFoldersNotInList(Context context, List<Long> ids) {
         int result = mContentResolver.delete(
                 ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
                 ProviderMeta.ProviderTableMeta._ID + " NOT IN (?)",

+ 132 - 134
src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java

@@ -46,13 +46,13 @@ import android.widget.ImageView;
 
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
-import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.lib.resources.status.OwnCloudVersion;
+import com.owncloud.android.lib.resources.files.ServerFileInterface;
+import com.owncloud.android.lib.resources.files.TrashbinFile;
 import com.owncloud.android.ui.TextDrawable;
 import com.owncloud.android.ui.adapter.DiskLruImageCache;
 import com.owncloud.android.ui.fragment.FileFragment;
@@ -71,7 +71,7 @@ import java.io.FileNotFoundException;
 import java.io.InputStream;
 import java.lang.ref.WeakReference;
 import java.net.URLEncoder;
-import java.util.ArrayList;
+import java.util.List;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
@@ -272,7 +272,7 @@ public class ThumbnailsCacheManager {
 
                     if (bitmap != null) {
                         // Handle PNG
-                        if (file.getMimetype().equalsIgnoreCase(PNG_MIMETYPE)) {
+                        if (file.getMimeType().equalsIgnoreCase(PNG_MIMETYPE)) {
                             bitmap = handlePNG(bitmap, pxW, pxH);
                         }
 
@@ -284,45 +284,39 @@ public class ThumbnailsCacheManager {
 
                 } else {
                     // Download thumbnail from server
-                    OwnCloudVersion serverOCVersion = AccountUtils.getServerVersion(account);
-
                     if (mClient != null) {
-                        if (serverOCVersion.supportsRemoteThumbnails()) {
-                            GetMethod getMethod = null;
-                            try {
-                                String uri = mClient.getBaseUri() + "/index.php/core/preview.png?file="
-                                        + URLEncoder.encode(file.getRemotePath())
-                                        + "&x=" + pxW + "&y=" + pxH + "&a=1&mode=cover&forceIcon=0";
-                                getMethod = new GetMethod(uri);
-
-                                int status = mClient.executeMethod(getMethod);
-                                if (status == HttpStatus.SC_OK) {
-                                    InputStream inputStream = getMethod.getResponseBodyAsStream();
-                                    thumbnail = BitmapFactory.decodeStream(inputStream);
-                                } else {
-                                    mClient.exhaustResponse(getMethod.getResponseBodyAsStream());
-                                }
+                        GetMethod getMethod = null;
+                        try {
+                            String uri = mClient.getBaseUri() + "/index.php/core/preview.png?file="
+                                    + URLEncoder.encode(file.getRemotePath())
+                                    + "&x=" + pxW + "&y=" + pxH + "&a=1&mode=cover&forceIcon=0";
+                            getMethod = new GetMethod(uri);
+
+                            int status = mClient.executeMethod(getMethod);
+                            if (status == HttpStatus.SC_OK) {
+                                InputStream inputStream = getMethod.getResponseBodyAsStream();
+                                thumbnail = BitmapFactory.decodeStream(inputStream);
+                            } else {
+                                mClient.exhaustResponse(getMethod.getResponseBodyAsStream());
+                            }
 
                                 // Handle PNG
-                                if (thumbnail != null && file.getMimetype().equalsIgnoreCase(PNG_MIMETYPE)) {
+                                if (thumbnail != null && file.getMimeType().equalsIgnoreCase(PNG_MIMETYPE)) {
                                     thumbnail = handlePNG(thumbnail, thumbnail.getWidth(), thumbnail.getHeight());
                                 }
 
-                                // Add thumbnail to cache
-                                if (thumbnail != null) {
-                                    Log_OC.d(TAG, "add thumbnail to cache: " + file.getFileName());
-                                    addBitmapToCache(imageKey, thumbnail);
-                                }
+                            // Add thumbnail to cache
+                            if (thumbnail != null) {
+                                Log_OC.d(TAG, "add thumbnail to cache: " + file.getFileName());
+                                addBitmapToCache(imageKey, thumbnail);
+                            }
 
-                            } catch (Exception e) {
-                                Log_OC.d(TAG, e.getMessage(), e);
-                            } finally {
-                                if (getMethod != null) {
-                                    getMethod.releaseConnection();
-                                }
+                        } catch (Exception e) {
+                            Log_OC.d(TAG, e.getMessage(), e);
+                        } finally {
+                            if (getMethod != null) {
+                                getMethod.releaseConnection();
                             }
-                        } else {
-                            Log_OC.d(TAG, "Server too old");
                         }
                     }
                 }
@@ -385,7 +379,7 @@ public class ThumbnailsCacheManager {
     public static class ThumbnailGenerationTask extends AsyncTask<ThumbnailGenerationTaskObject, Void, Bitmap> {
         private final WeakReference<ImageView> mImageViewReference;
         private static Account mAccount;
-        private ArrayList<ThumbnailGenerationTask> mAsyncTasks = null;
+        private List<ThumbnailGenerationTask> mAsyncTasks;
         private Object mFile;
         private String mImageKey = null;
         private FileDataStorageManager mStorageManager;
@@ -397,7 +391,7 @@ public class ThumbnailsCacheManager {
         }
 
         public ThumbnailGenerationTask(ImageView imageView, FileDataStorageManager storageManager,
-                                       Account account, ArrayList<ThumbnailGenerationTask> asyncTasks)
+                                       Account account, List<ThumbnailGenerationTask> asyncTasks)
                 throws IllegalArgumentException {
             // Use a WeakReference to ensure the ImageView can be garbage collected
             mImageViewReference = new WeakReference<ImageView>(imageView);
@@ -446,10 +440,10 @@ public class ThumbnailsCacheManager {
                 mFile = object.getFile();
                 mImageKey = object.getImageKey();
 
-                if (mFile instanceof OCFile) {
+                if (mFile instanceof ServerFileInterface) {
                     thumbnail = doThumbnailFromOCFileInBackground();
 
-                    if (MimeTypeUtil.isVideo((OCFile) mFile) && thumbnail != null) {
+                    if (MimeTypeUtil.isVideo((ServerFileInterface) mFile) && thumbnail != null) {
                         thumbnail = addVideoOverlay(thumbnail);
                     }
                 } else if (mFile instanceof File) {
@@ -484,6 +478,8 @@ public class ThumbnailsCacheManager {
                         tagId = String.valueOf(((OCFile)mFile).getFileId());
                     } else if (mFile instanceof File) {
                         tagId = String.valueOf(mFile.hashCode());
+                    } else if (mFile instanceof TrashbinFile) {
+                        tagId = String.valueOf(((TrashbinFile) mFile).getRemoteId());
                     }
                     if (String.valueOf(imageView.getTag()).equals(tagId)) {
                         imageView.setImageBitmap(bitmap);
@@ -498,40 +494,44 @@ public class ThumbnailsCacheManager {
 
         private Bitmap doThumbnailFromOCFileInBackground() {
             Bitmap thumbnail;
-            OCFile file = (OCFile) mFile;
+            ServerFileInterface file = (ServerFileInterface) mFile;
             String imageKey = PREFIX_THUMBNAIL + String.valueOf(file.getRemoteId());
 
             // Check disk cache in background thread
             thumbnail = getBitmapFromDiskCache(imageKey);
 
             // Not found in disk cache
-            if (thumbnail == null || file.needsUpdateThumbnail()) {
+            if (thumbnail == null || (file instanceof OCFile && ((OCFile) file).needsUpdateThumbnail())) {
                 int pxW;
                 int pxH;
                 pxW = pxH = getThumbnailDimension();
 
-                if (file.isDown()) {
-                    Bitmap bitmap;
-                    if (MimeTypeUtil.isVideo(file)) {
-                        bitmap = ThumbnailUtils.createVideoThumbnail(file.getStoragePath(),
-                                MediaStore.Images.Thumbnails.MINI_KIND);
-                    } else {
-                        bitmap = BitmapUtils.decodeSampledBitmapFromFile(file.getStoragePath(), pxW, pxH);
-                    }
-
-                    if (bitmap != null) {
-                        // Handle PNG
-                        if (file.getMimetype().equalsIgnoreCase(PNG_MIMETYPE)) {
-                            bitmap = handlePNG(bitmap, pxW, pxH);
+                if (file instanceof OCFile) {
+                    OCFile ocFile = (OCFile) file;
+                    if (ocFile.isDown()) {
+                        Bitmap bitmap;
+                        if (MimeTypeUtil.isVideo(ocFile)) {
+                            bitmap = ThumbnailUtils.createVideoThumbnail(ocFile.getStoragePath(),
+                                    MediaStore.Images.Thumbnails.MINI_KIND);
+                        } else {
+                            bitmap = BitmapUtils.decodeSampledBitmapFromFile(ocFile.getStoragePath(), pxW, pxH);
                         }
 
-                        thumbnail = addThumbnailToCache(imageKey, bitmap, file.getStoragePath(), pxW, pxH);
+                        if (bitmap != null) {
+                            // Handle PNG
+                            if (ocFile.getMimeType().equalsIgnoreCase(PNG_MIMETYPE)) {
+                                bitmap = handlePNG(bitmap, pxW, pxH);
+                            }
 
-                        file.setNeedsUpdateThumbnail(false);
-                        mStorageManager.saveFile(file);
+                            thumbnail = addThumbnailToCache(imageKey, bitmap, ocFile.getStoragePath(), pxW, pxH);
+
+                            ocFile.setNeedsUpdateThumbnail(false);
+                            mStorageManager.saveFile(ocFile);
+                        }
                     }
+                }
 
-                } else {
+                if (thumbnail == null) {
                     // check if resized version is available
                     String resizedImageKey = PREFIX_RESIZED_IMAGE + String.valueOf(file.getRemoteId());
                     Bitmap resizedImage = getBitmapFromDiskCache(resizedImageKey);
@@ -540,14 +540,20 @@ public class ThumbnailsCacheManager {
                         thumbnail = ThumbnailUtils.extractThumbnail(resizedImage, pxW, pxH);
                     } else {
                         // Download thumbnail from server
-                        if (mClient != null && AccountUtils.getServerVersion(mAccount).supportsRemoteThumbnails()) {
+                        if (mClient != null) {
                             getMethod = null;
                             try {
                                 // thumbnail
-                                String uri = mClient.getBaseUri() + "/index.php/apps/files/api/v1/thumbnail/" +
-                                        pxW + "/" + pxH + Uri.encode(file.getRemotePath(), "/");
-                                Log_OC.d(TAG, "generate thumbnail: " + file.getFileName() +
-                                        " URI: " + uri);
+                                String uri;
+                                if (file instanceof OCFile) {
+                                    uri = mClient.getBaseUri() + "/index.php/apps/files/api/v1/thumbnail/" +
+                                            pxW + "/" + pxH + Uri.encode(file.getRemotePath(), "/");
+                                } else {
+                                    uri = mClient.getBaseUri() + "/index.php/apps/files_trashbin/preview?fileId=" +
+                                            file.getLocalId() + "&x=" + pxW + "&y=" + pxH;
+                                }
+                                
+                                Log_OC.d(TAG, "generate thumbnail: " + file.getFileName() + " URI: " + uri);
                                 getMethod = new GetMethod(uri);
                                 getMethod.setRequestHeader("Cookie",
                                         "nc_sameSiteCookielax=true;nc_sameSiteCookiestrict=true");
@@ -565,7 +571,7 @@ public class ThumbnailsCacheManager {
                                 }
 
                                 // Handle PNG
-                                if (file.getMimetype().equalsIgnoreCase(PNG_MIMETYPE)) {
+                                if (file.getMimeType().equalsIgnoreCase(PNG_MIMETYPE)) {
                                     thumbnail = handlePNG(thumbnail, pxW, pxH);
                                 }
                             } catch (Exception e) {
@@ -575,8 +581,6 @@ public class ThumbnailsCacheManager {
                                     getMethod.releaseConnection();
                                 }
                             }
-                        } else {
-                            Log_OC.d(TAG, "Server too old");
                         }
                     }
 
@@ -589,7 +593,6 @@ public class ThumbnailsCacheManager {
             }
 
             return thumbnail;
-
         }
 
         /**
@@ -778,13 +781,10 @@ public class ThumbnailsCacheManager {
 
 
         public AvatarGenerationTask(AvatarGenerationListener avatarGenerationListener, Object callContext,
-                                    FileDataStorageManager storageManager, Account account, Resources resources,
-                                    float avatarRadius, String userId, String serverName, Context context) {
+                                    Account account, Resources resources, float avatarRadius, String userId,
+                                    String serverName, Context context) {
             mAvatarGenerationListener = new WeakReference<>(avatarGenerationListener);
             mCallContext = callContext;
-            if (storageManager == null) {
-                throw new IllegalArgumentException("storageManager must not be NULL");
-            }
             mAccount = account;
             mResources = resources;
             mAvatarRadius = avatarRadius;
@@ -806,9 +806,9 @@ public class ThumbnailsCacheManager {
 
                 thumbnail = doAvatarInBackground();
 
-            } catch(OutOfMemoryError oome) {
+            } catch (OutOfMemoryError oome) {
                 Log_OC.e(TAG, "Out of memory");
-            } catch(Throwable t){
+            } catch (Throwable t) {
                 // the app should never break due to a problem with avatars
                 Log_OC.e(TAG, "Generation of avatar for " + mUserId + " failed", t);
             }
@@ -829,6 +829,7 @@ public class ThumbnailsCacheManager {
 
         /**
          * Converts size of file icon from dp to pixel
+         *
          * @return int
          */
         private int getAvatarDimension() {
@@ -851,81 +852,77 @@ public class ThumbnailsCacheManager {
             int px = getAvatarDimension();
 
             // Download avatar from server
-            OwnCloudVersion serverOCVersion = AccountUtils.getServerVersion(mAccount);
             if (mClient != null) {
-                if (serverOCVersion.supportsRemoteThumbnails()) {
-                    GetMethod get = null;
-                    try {
-                        String uri = mClient.getBaseUri() + "/index.php/avatar/" + Uri.encode(mUserId) + "/" + px;
-                        Log_OC.d("Avatar", "URI: " + uri);
-                        get = new GetMethod(uri);
-
-                        // only use eTag if available and corresponding avatar is still there 
-                        // (might be deleted from cache)
-                        if (!eTag.isEmpty() && getBitmapFromDiskCache(avatarKey) != null) {
-                            get.setRequestHeader("If-None-Match", eTag);
-                        }
+                GetMethod get = null;
+                try {
+                    String uri = mClient.getBaseUri() + "/index.php/avatar/" + Uri.encode(mUserId) + "/" + px;
+                    Log_OC.d("Avatar", "URI: " + uri);
+                    get = new GetMethod(uri);
+
+                    // only use eTag if available and corresponding avatar is still there 
+                    // (might be deleted from cache)
+                    if (!eTag.isEmpty() && getBitmapFromDiskCache(avatarKey) != null) {
+                        get.setRequestHeader("If-None-Match", eTag);
+                    }
 
-                        int status = mClient.executeMethod(get);
+                    int status = mClient.executeMethod(get);
 
-                        // we are using eTag to download a new avatar only if it changed
-                        switch (status) {
-                            case HttpStatus.SC_OK:
-                                // new avatar
-                                InputStream inputStream = get.getResponseBodyAsStream();
+                    // we are using eTag to download a new avatar only if it changed
+                    switch (status) {
+                        case HttpStatus.SC_OK:
+                            // new avatar
+                            InputStream inputStream = get.getResponseBodyAsStream();
 
-                                String newETag = null;
-                                if (get.getResponseHeader(ETAG) != null) {
-                                    newETag = get.getResponseHeader(ETAG).getValue().replace("\"", "");
-                                    arbitraryDataProvider.storeOrUpdateKeyValue(accountName, AVATAR, newETag);
-                                }
+                            String newETag = null;
+                            if (get.getResponseHeader(ETAG) != null) {
+                                newETag = get.getResponseHeader(ETAG).getValue().replace("\"", "");
+                                arbitraryDataProvider.storeOrUpdateKeyValue(accountName, AVATAR, newETag);
+                            }
 
-                                Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
-                                avatar = ThumbnailUtils.extractThumbnail(bitmap, px, px);
+                            Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
+                            avatar = ThumbnailUtils.extractThumbnail(bitmap, px, px);
 
-                                // Add avatar to cache
-                                if (avatar != null && !TextUtils.isEmpty(newETag)) {
-                                    avatar = handlePNG(avatar, px, px);
-                                    String newImageKey = "a_" + mUserId + "_" + mServerName + "_" + newETag;
-                                    addBitmapToCache(newImageKey, avatar);
-                                } else {
-                                    return TextDrawable.createAvatar(mAccount.name, mAvatarRadius);
-                                }
-                                break;
+                            // Add avatar to cache
+                            if (avatar != null && !TextUtils.isEmpty(newETag)) {
+                                avatar = handlePNG(avatar, px, px);
+                                String newImageKey = "a_" + mUserId + "_" + mServerName + "_" + newETag;
+                                addBitmapToCache(newImageKey, avatar);
+                            } else {
+                                return TextDrawable.createAvatar(mAccount.name, mAvatarRadius);
+                            }
+                            break;
 
-                            case HttpStatus.SC_NOT_MODIFIED:
-                                // old avatar
-                                avatar = getBitmapFromDiskCache(avatarKey);
-                                mClient.exhaustResponse(get.getResponseBodyAsStream());
-                                break;
+                        case HttpStatus.SC_NOT_MODIFIED:
+                            // old avatar
+                            avatar = getBitmapFromDiskCache(avatarKey);
+                            mClient.exhaustResponse(get.getResponseBodyAsStream());
+                            break;
 
-                            default:
-                                // everything else
-                                mClient.exhaustResponse(get.getResponseBodyAsStream());
-                                break;
+                        default:
+                            // everything else
+                            mClient.exhaustResponse(get.getResponseBodyAsStream());
+                            break;
 
-                        }
-                    } catch (Exception e) {
-                        try {
-                            return TextDrawable.createAvatar(mAccount.name, mAvatarRadius);
-                        } catch (Exception e1) {
-                            Log_OC.e(TAG, "Error generating fallback avatar");
-                        }
-                    } finally {
-                        if (get != null) {
-                            get.releaseConnection();
-                        }
                     }
-                } else {
-                    Log_OC.d(TAG, "Server too old");
-
+                } catch (Exception e) {
                     try {
                         return TextDrawable.createAvatar(mAccount.name, mAvatarRadius);
-                    } catch (Exception e) {
+                    } catch (Exception e1) {
                         Log_OC.e(TAG, "Error generating fallback avatar");
                     }
+                } finally {
+                    if (get != null) {
+                        get.releaseConnection();
+                    }
+                }
+
+                try {
+                    return TextDrawable.createAvatar(mAccount.name, mAvatarRadius);
+                } catch (Exception e) {
+                    Log_OC.e(TAG, "Error generating fallback avatar");
                 }
             }
+
             return BitmapUtils.bitmapToCircularBitmapDrawable(mResources, avatar);
         }
     }
@@ -1081,6 +1078,7 @@ public class ThumbnailsCacheManager {
         return null;
     }
 
+
     public static class AsyncThumbnailDrawable extends BitmapDrawable {
         private final WeakReference<ThumbnailGenerationTask> bitmapWorkerTaskReference;
 
@@ -1154,7 +1152,7 @@ public class ThumbnailsCacheManager {
 
         if (bitmap != null) {
             // Handle PNG
-            if (file.getMimetype().equalsIgnoreCase(PNG_MIMETYPE)) {
+            if (file.getMimeType().equalsIgnoreCase(PNG_MIMETYPE)) {
                 bitmap = handlePNG(bitmap, pxW, pxH);
             }
 

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

@@ -79,7 +79,7 @@ public class UploadsStorageManager extends Observable {
         cv.put(ProviderTableMeta.UPLOADS_FORCE_OVERWRITE, ocUpload.isForceOverwrite() ? 1 : 0);
         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_CREATED_BY, ocUpload.getCreatedBy());
         cv.put(ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY, ocUpload.isWhileChargingOnly() ? 1 : 0);
         cv.put(ProviderTableMeta.UPLOADS_IS_WIFI_ONLY, ocUpload.isUseWifiOnly() ? 1 : 0);
         cv.put(ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN, ocUpload.getFolderUnlockToken());
@@ -320,7 +320,7 @@ public class UploadsStorageManager extends Observable {
             upload.setUploadStatus(
                     UploadStatus.fromValue(c.getInt(c.getColumnIndex(ProviderTableMeta.UPLOADS_STATUS)))
             );
-            upload.setLocalAction(c.getInt(c.getColumnIndex((ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR))));
+            upload.setLocalAction(c.getInt(c.getColumnIndex(ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR)));
             upload.setForceOverwrite(c.getInt(
                     c.getColumnIndex(ProviderTableMeta.UPLOADS_FORCE_OVERWRITE)) == 1);
             upload.setCreateRemoteFolder(c.getInt(

+ 1 - 2
src/main/java/com/owncloud/android/datastorage/DataStorageProvider.java

@@ -35,14 +35,13 @@ import com.owncloud.android.datastorage.providers.VDCStoragePointProvider;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Vector;
 
 /**
  * @author Bartosz Przybylski
  */
 public class DataStorageProvider {
 
-    private static final Vector<IStoragePointProvider> mStorageProviders = new Vector<>();
+    private static final List<IStoragePointProvider> mStorageProviders = new ArrayList<>();
     private static final UniqueStorageList mCachedStoragePoints = new UniqueStorageList();
     private static final DataStorageProvider sInstance = new DataStorageProvider() {{
         // There is no system wide way to get usb storage so we need to provide multiple

+ 136 - 103
src/main/java/com/owncloud/android/db/OCUpload.java

@@ -1,10 +1,12 @@
-/**
+/*
  * ownCloud Android client application
  *
  * @author LukeOwncloud
  * @author masensio
  * @author David A. Velasco
+ * @author Tobias Kaminsky
  * Copyright (C) 2016 ownCloud Inc.
+ * Copyright (C) 2018 Nextcloud 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,
@@ -45,77 +47,85 @@ public class OCUpload implements Parcelable {
 
     private static final String TAG = OCUpload.class.getSimpleName();
 
-    private long mId;
+    private long id;
 
     /**
      * Absolute path in the local file system to the file to be uploaded.
      */
-    private String mLocalPath;
+    private String localPath;
 
     /**
      * Absolute path in the remote account to set to the uploaded file (not for its parent folder!)
      */
-    private String mRemotePath;
+    private String remotePath;
 
     /**
      * Name of Owncloud account to upload file to.
      */
-    private String mAccountName;
+    private String accountName;
 
     /**
      * File size.
      */
-    private long mFileSize;
+    private long fileSize;
 
     /**
      * Local action for upload. (0 - COPY, 1 - MOVE, 2 - FORGET)
      */
-    private int mLocalAction;
+    private int localAction;
 
     /**
      * Overwrite destination file?
      */
-    private boolean mForceOverwrite;
+    private boolean forceOverwrite;
 
     /**
      * Create destination folder?
      */
-    private boolean mIsCreateRemoteFolder;
+    private boolean createRemoteFolder;
 
     /**
      * Status of upload (later, in_progress, ...).
      */
-    private UploadStatus mUploadStatus;
+    private UploadStatus uploadStatus;
 
     /**
      * Result from last upload operation. Can be null.
      */
-    private UploadResult mLastResult;
+    private UploadResult lastResult;
 
     /**
      * Defines the origin of the upload; see constants CREATED_ in {@link UploadFileOperation}
      */
-    private int mCreatedBy;
+    private int createdBy;
 
     /**
      * When the upload ended
      */
-    private long mUploadEndTimeStamp;
+    private long uploadEndTimeStamp;
 
     /**
      * Upload only via wifi?
      */
-    private boolean mIsUseWifiOnly;
+    private boolean useWifiOnly;
 
     /**
      * Upload only if phone being charged?
      */
-    private boolean mIsWhileChargingOnly;
+    private boolean whileChargingOnly;
 
     /**
      * Token to unlock E2E folder
      */
-    private String mFolderUnlockToken;
+    private String folderUnlockToken;
+
+    /**
+     * temporary values, used for sorting
+     */
+    private UploadStatus fixedUploadStatus;
+    private boolean fixedUploadingNow;
+    private long fixedUploadEndTimeStamp;
+    private long fixedId;
 
     /**
      * Main constructor.
@@ -135,13 +145,13 @@ public class OCUpload implements Parcelable {
             throw new IllegalArgumentException("Invalid account name");
         }
         resetData();
-        mLocalPath = localPath;
-        mRemotePath = remotePath;
-        mAccountName = accountName;
+        this.localPath = localPath;
+        this.remotePath = remotePath;
+        this.accountName = accountName;
     }
 
     /**
-     * Convenience constructor to reupload already existing {@link OCFile}s.
+     * Convenience constructor to re-upload 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.
@@ -154,36 +164,43 @@ public class OCUpload implements Parcelable {
      * Reset all the fields to default values.
      */
     private void resetData() {
-        mRemotePath = "";
-        mLocalPath = "";
-        mAccountName = "";
-        mFileSize = -1;
-        mId = -1;
-        mLocalAction = FileUploader.LOCAL_BEHAVIOUR_COPY;
-        mForceOverwrite = false;
-        mIsCreateRemoteFolder = false;
-        mUploadStatus = UploadStatus.UPLOAD_IN_PROGRESS;
-        mLastResult = UploadResult.UNKNOWN;
-        mCreatedBy = UploadFileOperation.CREATED_BY_USER;
-        mIsUseWifiOnly = true;
-        mIsWhileChargingOnly = false;
-        mFolderUnlockToken = "";
+        remotePath = "";
+        localPath = "";
+        accountName = "";
+        fileSize = -1;
+        id = -1;
+        localAction = FileUploader.LOCAL_BEHAVIOUR_COPY;
+        forceOverwrite = false;
+        createRemoteFolder = false;
+        uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS;
+        lastResult = UploadResult.UNKNOWN;
+        createdBy = UploadFileOperation.CREATED_BY_USER;
+        useWifiOnly = true;
+        whileChargingOnly = false;
+        folderUnlockToken = "";
+    }
+
+    public void setDataFixed(FileUploader.FileUploaderBinder binder) {
+        fixedUploadStatus = uploadStatus;
+        fixedUploadingNow = binder != null && binder.isUploadingNow(this);
+        fixedUploadEndTimeStamp = uploadEndTimeStamp;
+        fixedId = id;
     }
 
     // Getters & Setters
     public void setUploadId(long id) {
-        mId = id;
+        this.id = id;
     }
 
     public long getUploadId() {
-        return mId;
+        return id;
     }
 
     /**
      * @return the uploadStatus
      */
     public UploadStatus getUploadStatus() {
-        return mUploadStatus;
+        return uploadStatus;
     }
 
     /**
@@ -191,7 +208,7 @@ public class OCUpload implements Parcelable {
      * @param uploadStatus the uploadStatus to set
      */
     public void setUploadStatus(UploadStatus uploadStatus) {
-        this.mUploadStatus = uploadStatus;
+        this.uploadStatus = uploadStatus;
         setLastResult(UploadResult.UNKNOWN);
     }
 
@@ -199,14 +216,14 @@ public class OCUpload implements Parcelable {
      * @return the lastResult
      */
     public UploadResult getLastResult() {
-        return mLastResult;
+        return lastResult;
     }
 
     /**
      * @param lastResult the lastResult to set
      */
     public void setLastResult(UploadResult lastResult) {
-        this.mLastResult = ((lastResult != null) ? lastResult : UploadResult.UNKNOWN);
+        this.lastResult = lastResult != null ? lastResult : UploadResult.UNKNOWN;
     }
 
 
@@ -214,25 +231,25 @@ public class OCUpload implements Parcelable {
      * @return the localPath
      */
     public String getLocalPath() {
-        return mLocalPath;
+        return localPath;
     }
 
     public void setLocalPath(String localPath) {
-        mLocalPath = localPath;
+        this.localPath = localPath;
     }
 
     /**
      * @return the remotePath
      */
     public String getRemotePath() {
-        return mRemotePath;
+        return remotePath;
     }
 
     /**
-     * @param remotePath
+     * @param remotePath the remotePath
      */
     public void setRemotePath(String remotePath) {
-        mRemotePath = remotePath;
+        this.remotePath = remotePath;
     }
 
 
@@ -240,67 +257,67 @@ public class OCUpload implements Parcelable {
      * @return File size
      */
     public long getFileSize() {
-        return mFileSize;
+        return fileSize;
     }
 
     public void setFileSize(long fileSize) {
-        mFileSize = fileSize;
+        this.fileSize = fileSize;
     }
 
     /**
      * @return the mimeType
      */
     public String getMimeType() {
-        return MimeTypeUtil.getBestMimeTypeByFilename(mLocalPath);
+        return MimeTypeUtil.getBestMimeTypeByFilename(localPath);
     }
 
     /**
      * @return the localAction
      */
     public int getLocalAction() {
-        return mLocalAction;
+        return localAction;
     }
 
     /**
      * @param localAction the localAction to set
      */
     public void setLocalAction(int localAction) {
-        this.mLocalAction = localAction;
+        this.localAction = localAction;
     }
 
     /**
      * @return the forceOverwrite
      */
     public boolean isForceOverwrite() {
-        return mForceOverwrite;
+        return forceOverwrite;
     }
 
     /**
      * @param forceOverwrite the forceOverwrite to set
      */
     public void setForceOverwrite(boolean forceOverwrite) {
-        this.mForceOverwrite = forceOverwrite;
+        this.forceOverwrite = forceOverwrite;
     }
 
     /**
      * @return the isCreateRemoteFolder
      */
     public boolean isCreateRemoteFolder() {
-        return mIsCreateRemoteFolder;
+        return createRemoteFolder;
     }
 
     /**
-     * @param isCreateRemoteFolder the isCreateRemoteFolder to set
+     * @param createRemoteFolder the createRemoteFolder to set
      */
-    public void setCreateRemoteFolder(boolean isCreateRemoteFolder) {
-        this.mIsCreateRemoteFolder = isCreateRemoteFolder;
+    public void setCreateRemoteFolder(boolean createRemoteFolder) {
+        this.createRemoteFolder = createRemoteFolder;
     }
 
     /**
      * @return the accountName
      */
     public String getAccountName() {
-        return mAccountName;
+        return accountName;
     }
 
     /**
@@ -311,19 +328,19 @@ public class OCUpload implements Parcelable {
     }
 
     public void setCreatedBy(int createdBy) {
-        mCreatedBy = createdBy;
+        this.createdBy = createdBy;
     }
 
-    public int getCreadtedBy() {
-        return mCreatedBy;
+    public int getCreatedBy() {
+        return createdBy;
     }
 
     public void setUploadEndTimestamp(long uploadEndTimestamp) {
-        mUploadEndTimeStamp = uploadEndTimestamp;
+        uploadEndTimeStamp = uploadEndTimestamp;
     }
 
     public long getUploadEndTimestamp() {
-        return mUploadEndTimeStamp;
+        return uploadEndTimeStamp;
     }
 
     /**
@@ -360,30 +377,30 @@ public class OCUpload implements Parcelable {
      * @return the isUseWifiOnly
      */
     public boolean isUseWifiOnly() {
-        return mIsUseWifiOnly;
+        return useWifiOnly;
     }
 
     /**
-     * @param isUseWifiOnly the isUseWifiOnly to set
+     * @param useWifiOnly the useWifiOnly to set
      */
-    public void setUseWifiOnly(boolean isUseWifiOnly) {
-        this.mIsUseWifiOnly = isUseWifiOnly;
+    public void setUseWifiOnly(boolean useWifiOnly) {
+        this.useWifiOnly = useWifiOnly;
     }
 
-    public void setWhileChargingOnly(boolean isWhileChargingOnly) {
-        this.mIsWhileChargingOnly = isWhileChargingOnly;
+    public void setWhileChargingOnly(boolean whileChargingOnly) {
+        this.whileChargingOnly = whileChargingOnly;
     }
 
     public boolean isWhileChargingOnly() {
-        return mIsWhileChargingOnly;
+        return whileChargingOnly;
     }
 
     public void setFolderUnlockToken(String token) {
-        mFolderUnlockToken = token;
+        folderUnlockToken = token;
     }
 
     public String getFolderUnlockToken() {
-        return mFolderUnlockToken;
+        return folderUnlockToken;
     }
 
     /**
@@ -391,33 +408,33 @@ public class OCUpload implements Parcelable {
      *
      * @param source The source parcel
      */
-    protected OCUpload(Parcel source) {
+    private OCUpload(Parcel source) {
         readFromParcel(source);
     }
 
-    public void readFromParcel(Parcel source) {
-        mId = source.readLong();
-        mLocalPath = source.readString();
-        mRemotePath = source.readString();
-        mAccountName = source.readString();
-        mLocalAction = source.readInt();
-        mForceOverwrite = (source.readInt() == 1);
-        mIsCreateRemoteFolder = (source.readInt() == 1);
+    private void readFromParcel(Parcel source) {
+        id = source.readLong();
+        localPath = source.readString();
+        remotePath = source.readString();
+        accountName = source.readString();
+        localAction = source.readInt();
+        forceOverwrite = source.readInt() == 1;
+        createRemoteFolder = source.readInt() == 1;
         try {
-            mUploadStatus = UploadStatus.valueOf(source.readString());
+            uploadStatus = UploadStatus.valueOf(source.readString());
         } catch (IllegalArgumentException x) {
-            mUploadStatus = UploadStatus.UPLOAD_IN_PROGRESS;
+            uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS;
         }
-        mUploadEndTimeStamp = source.readLong();
+        uploadEndTimeStamp = source.readLong();
         try {
-            mLastResult = UploadResult.valueOf(source.readString());
+            lastResult = UploadResult.valueOf(source.readString());
         } catch (IllegalArgumentException x) {
-            mLastResult = UploadResult.UNKNOWN;
+            lastResult = UploadResult.UNKNOWN;
         }
-        mCreatedBy = source.readInt();
-        mIsUseWifiOnly = (source.readInt() == 1);
-        mIsWhileChargingOnly = (source.readInt() == 1);
-        mFolderUnlockToken = source.readString();
+        createdBy = source.readInt();
+        useWifiOnly = source.readInt() == 1;
+        whileChargingOnly = source.readInt() == 1;
+        folderUnlockToken = source.readString();
     }
 
     @Override
@@ -427,20 +444,36 @@ public class OCUpload implements Parcelable {
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
-        dest.writeLong(mId);
-        dest.writeString(mLocalPath);
-        dest.writeString(mRemotePath);
-        dest.writeString(mAccountName);
-        dest.writeInt(mLocalAction);
-        dest.writeInt(mForceOverwrite ? 1 : 0);
-        dest.writeInt(mIsCreateRemoteFolder ? 1 : 0);
-        dest.writeString(mUploadStatus.name());
-        dest.writeLong(mUploadEndTimeStamp);
-        dest.writeString(((mLastResult == null) ? "" : mLastResult.name()));
-        dest.writeInt(mCreatedBy);
-        dest.writeInt(mIsUseWifiOnly ? 1 : 0);
-        dest.writeInt(mIsWhileChargingOnly ? 1 : 0);
-        dest.writeString(mFolderUnlockToken);
+        dest.writeLong(id);
+        dest.writeString(localPath);
+        dest.writeString(remotePath);
+        dest.writeString(accountName);
+        dest.writeInt(localAction);
+        dest.writeInt(forceOverwrite ? 1 : 0);
+        dest.writeInt(createRemoteFolder ? 1 : 0);
+        dest.writeString(uploadStatus.name());
+        dest.writeLong(uploadEndTimeStamp);
+        dest.writeString(lastResult == null ? "" : lastResult.name());
+        dest.writeInt(createdBy);
+        dest.writeInt(useWifiOnly ? 1 : 0);
+        dest.writeInt(whileChargingOnly ? 1 : 0);
+        dest.writeString(folderUnlockToken);
+    }
+
+    public UploadStatus getFixedUploadStatus() {
+        return fixedUploadStatus;
+    }
+
+    public boolean isFixedUploadingNow() {
+        return fixedUploadingNow;
+    }
+
+    public long getFixedUploadEndTimestamp() {
+        return fixedUploadEndTimeStamp;
+    }
+
+    public Long getFixedUploadId() {
+        return fixedId;
     }
 
     enum CanUploadFileNowStatus {NOW, LATER, FILE_GONE, ERROR}

+ 2 - 2
src/main/java/com/owncloud/android/features/FeatureList.java

@@ -44,8 +44,8 @@ public class FeatureList {
     private static final int VERSION_3_3_0 = 30030099;
     private static final int BETA_VERSION_0 = 0;
 
-    static public ArrayList<FeatureItem> get(boolean isMultiAccount) {
-        ArrayList<FeatureItem> featuresList = new ArrayList<>();
+    static public List<FeatureItem> get(boolean isMultiAccount) {
+        List<FeatureItem> featuresList = new ArrayList<>();
         // Basic features showed on first install
         featuresList.add(new FeatureItem(R.drawable.whats_new_files,
                 R.string.welcome_feature_1_title, R.string.welcome_feature_1_text,

+ 17 - 1
src/main/java/com/owncloud/android/files/FileMenuFilter.java

@@ -27,6 +27,7 @@ import android.view.Menu;
 import android.view.MenuItem;
 
 import com.owncloud.android.R;
+import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
@@ -98,7 +99,7 @@ public class FileMenuFilter {
      * @param inSingleFileFragment  True if this is not listing, but single file fragment, like preview or details.
      */
     public void filter(Menu menu, boolean inSingleFileFragment) {
-        if (mFiles == null || mFiles.size() <= 0) {
+        if (mFiles == null || mFiles.isEmpty()) {
             hideAll(menu);
         } else {
             List<Integer> toShow = new ArrayList<>();
@@ -183,6 +184,7 @@ public class FileMenuFilter {
         filterEncrypt(toShow, toHide, endToEndEncryptionEnabled);
         filterUnsetEncrypted(toShow, toHide, endToEndEncryptionEnabled);
         filterSetPictureAs(toShow, toHide);
+        filterStream(toShow, toHide);
     }
 
     private void filterShareFile(List<Integer> toShow, List<Integer> toHide, OCCapability capability) {
@@ -346,6 +348,15 @@ public class FileMenuFilter {
         }
     }
 
+    private void filterStream(List<Integer> toShow, List<Integer> toHide) {
+        if (mFiles.isEmpty() || !isSingleFile() || !isSingleMedia() ||
+                !AccountUtils.getServerVersion(mAccount).isMediaStreamingSupported()) {
+            toHide.add(R.id.action_stream_media);
+        } else {
+            toShow.add(R.id.action_stream_media);
+        }
+    }
+
     private boolean anyFileSynchronizing() {
         boolean synchronizing = false;
         if (mComponentsGetter != null && !mFiles.isEmpty() && mAccount != null) {
@@ -430,6 +441,11 @@ public class FileMenuFilter {
         return isSingleSelection() && MimeTypeUtil.isImage(mFiles.iterator().next());
     }
 
+    private boolean isSingleMedia() {
+        OCFile file = mFiles.iterator().next();
+        return isSingleSelection() && (MimeTypeUtil.isVideo(file) || MimeTypeUtil.isAudio(file));
+    }
+
     private boolean allFiles() {
         return mFiles != null && !containsFolder();
     }

+ 91 - 0
src/main/java/com/owncloud/android/files/StreamMediaFileOperation.java

@@ -0,0 +1,91 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2018 Tobias Kaminsky
+ * Copyright (C) 2018 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.files;
+
+import com.owncloud.android.lib.common.OwnCloudClient;
+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 org.apache.commons.httpclient.HttpStatus;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class StreamMediaFileOperation extends RemoteOperation {
+    private static final String TAG = StreamMediaFileOperation.class.getSimpleName();
+    private static final int SYNC_READ_TIMEOUT = 40000;
+    private static final int SYNC_CONNECTION_TIMEOUT = 5000;
+    private static final String STREAM_MEDIA_URL = "/ocs/v2.php/apps/dav/api/v1/direct";
+    
+    private String fileID;
+
+    // JSON node names
+    private static final String NODE_OCS = "ocs";
+    private static final String NODE_DATA = "data";
+    private static final String NODE_URL = "url";
+    private static final String JSON_FORMAT = "?format=json";
+
+    public StreamMediaFileOperation(String fileID) {
+        this.fileID = fileID;
+    }
+
+    protected RemoteOperationResult run(OwnCloudClient client) {
+        RemoteOperationResult result;
+        PostMethod postMethod = null;
+
+        try {
+            postMethod = new PostMethod(client.getBaseUri() + STREAM_MEDIA_URL + JSON_FORMAT);
+            postMethod.setParameter("fileId", fileID);
+
+            // remote request
+            postMethod.addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE);
+
+            int status = client.executeMethod(postMethod, SYNC_READ_TIMEOUT, SYNC_CONNECTION_TIMEOUT);
+
+            if (status == HttpStatus.SC_OK) {
+                String response = postMethod.getResponseBodyAsString();
+
+                // Parse the response
+                JSONObject respJSON = new JSONObject(response);
+                String url = respJSON.getJSONObject(NODE_OCS).getJSONObject(NODE_DATA).getString(NODE_URL);
+
+                result = new RemoteOperationResult(true, postMethod);
+                ArrayList<Object> urlArray = new ArrayList<>();
+                urlArray.add(url);
+                result.setData(urlArray);
+            } else {
+                result = new RemoteOperationResult(false, postMethod);
+                client.exhaustResponse(postMethod.getResponseBodyAsStream());
+            }
+        } catch (Exception e) {
+            result = new RemoteOperationResult(e);
+            Log_OC.e(TAG, "Get stream url for file with id " + fileID + " failed: " + result.getLogMessage(),
+                    result.getException());
+        } finally {
+            if (postMethod != null) {
+                postMethod.releaseConnection();
+            }
+        }
+        return result;
+    }
+}

+ 11 - 11
src/main/java/com/owncloud/android/files/services/FileDownloader.java

@@ -91,13 +91,13 @@ public class FileDownloader extends Service
     private Looper mServiceLooper;
     private ServiceHandler mServiceHandler;
     private IBinder mBinder;
-    private OwnCloudClient mDownloadClient = null;
-    private Account mCurrentAccount = null;
+    private OwnCloudClient mDownloadClient;
+    private Account mCurrentAccount;
     private FileDataStorageManager mStorageManager;
 
     private IndexedForest<DownloadFileOperation> mPendingDownloads = new IndexedForest<>();
 
-    private DownloadFileOperation mCurrentDownload = null;
+    private DownloadFileOperation mCurrentDownload;
 
     private NotificationManager mNotificationManager;
     private NotificationCompat.Builder mNotificationBuilder;
@@ -134,7 +134,7 @@ public class FileDownloader extends Service
                 .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.notification_icon))
                 .setColor(ThemeUtils.primaryColor(getApplicationContext(), true));
 
-        if ((android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O)) {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
             builder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD);
         }
 
@@ -178,7 +178,7 @@ public class FileDownloader extends Service
 
         startForeground(FOREGROUND_SERVICE_ID, mNotification);
 
-        if (!intent.hasExtra(EXTRA_ACCOUNT) || !intent.hasExtra(EXTRA_FILE)) {
+        if (intent == null || !intent.hasExtra(EXTRA_ACCOUNT) || !intent.hasExtra(EXTRA_FILE)) {
             Log_OC.e(TAG, "Not enough information provided in intent");
             return START_NOT_STICKY;
         } else {
@@ -221,11 +221,11 @@ public class FileDownloader extends Service
     /**
      * Provides a binder object that clients can use to perform operations on the queue of downloads,
      * excepting the addition of new files.
-     * <p/>
+     *
      * Implemented to perform cancellation, pause and resume of existing downloads.
      */
     @Override
-    public IBinder onBind(Intent arg0) {
+    public IBinder onBind(Intent intent) {
         return mBinder;
     }
 
@@ -501,7 +501,7 @@ public class FileDownloader extends Service
         file.setEtag(mCurrentDownload.getEtag());
         file.setMimetype(mCurrentDownload.getMimeType());
         file.setStoragePath(mCurrentDownload.getSavePath());
-        file.setFileLength((new File(mCurrentDownload.getSavePath()).length()));
+        file.setFileLength(new File(mCurrentDownload.getSavePath()).length());
         file.setRemoteId(mCurrentDownload.getFile().getRemoteId());
         mStorageManager.saveFile(file);
         FileDataStorageManager.triggerMediaScan(file.getStoragePath());
@@ -528,7 +528,7 @@ public class FileDownloader extends Service
                                 new File(download.getSavePath()).getName())
                 );
 
-        if ((android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O)) {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
             mNotificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD);
         }
         
@@ -602,8 +602,8 @@ public class FileDownloader extends Service
             int tickerId = (downloadResult.isSuccess()) ? R.string.downloader_download_succeeded_ticker :
                     R.string.downloader_download_failed_ticker;
 
-            boolean needsToUpdateCredentials = (ResultCode.UNAUTHORIZED.equals(downloadResult.getCode()));
-            tickerId = (needsToUpdateCredentials) ?
+            boolean needsToUpdateCredentials = ResultCode.UNAUTHORIZED.equals(downloadResult.getCode());
+            tickerId = needsToUpdateCredentials ?
                     R.string.downloader_download_failed_credentials_error : tickerId;
 
             mNotificationBuilder

+ 25 - 28
src/main/java/com/owncloud/android/files/services/FileUploader.java

@@ -169,18 +169,18 @@ public class FileUploader extends Service
     private Looper mServiceLooper;
     private ServiceHandler mServiceHandler;
     private IBinder mBinder;
-    private OwnCloudClient mUploadClient = null;
-    private Account mCurrentAccount = null;
+    private OwnCloudClient mUploadClient;
+    private Account mCurrentAccount;
     private FileDataStorageManager mStorageManager;
     //since there can be only one instance of an Android service, there also just one db connection.
-    private UploadsStorageManager mUploadsStorageManager = null;
+    private UploadsStorageManager mUploadsStorageManager;
 
-    private IndexedForest<UploadFileOperation> mPendingUploads = new IndexedForest<UploadFileOperation>();
+    private IndexedForest<UploadFileOperation> mPendingUploads = new IndexedForest<>();
 
     /**
      * {@link UploadFileOperation} object of ongoing upload. Can be null. Note: There can only be one concurrent upload!
      */
-    private UploadFileOperation mCurrentUpload = null;
+    private UploadFileOperation mCurrentUpload;
 
     private NotificationManager mNotificationManager;
     private NotificationCompat.Builder mNotificationBuilder;
@@ -469,7 +469,7 @@ public class FileUploader extends Service
                 .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.notification_icon))
                 .setColor(ThemeUtils.primaryColor(getApplicationContext(), true));
 
-        if ((android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O)) {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
             builder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD);
         }
 
@@ -545,7 +545,6 @@ public class FileUploader extends Service
         boolean retry = intent.getBooleanExtra(KEY_RETRY, false);
         AbstractList<String> requestedUploads = new Vector<>();
 
-        boolean chunked = AccountUtils.getServerVersion(account).isChunkedUploadSupported();
         boolean onWifiOnly = intent.getBooleanExtra(KEY_WHILE_ON_WIFI_ONLY, false);
         boolean whileChargingOnly = intent.getBooleanExtra(KEY_WHILE_CHARGING_ONLY, false);
 
@@ -602,7 +601,7 @@ public class FileUploader extends Service
                     files[i] = UploadFileOperation.obtainNewOCFileToUpload(
                             remotePaths[i],
                             localPaths[i],
-                            ((mimeTypes != null) ? mimeTypes[i] : null)
+                            mimeTypes != null ? mimeTypes[i] : null
                     );
                     if (files[i] == null) {
                         Log_OC.e(TAG, "obtainNewOCFileToUpload() returned null for remotePaths[i]:" + remotePaths[i]
@@ -613,13 +612,13 @@ public class FileUploader extends Service
             }
             // at this point variable "OCFile[] files" is loaded correctly.
 
-            String uploadKey = null;
-            UploadFileOperation newUpload = null;
+            String uploadKey;
+            UploadFileOperation newUpload;
             try {
-                for (int i = 0; i < files.length; i++) {
+                for (OCFile file : files) {
 
-                    OCUpload ocUpload = new OCUpload(files[i], account);
-                    ocUpload.setFileSize(files[i].getFileLength());
+                    OCUpload ocUpload = new OCUpload(file, account);
+                    ocUpload.setFileSize(file.getFileLength());
                     ocUpload.setForceOverwrite(forceOverwrite);
                     ocUpload.setCreateRemoteFolder(isCreateRemoteFolder);
                     ocUpload.setCreatedBy(createdBy);
@@ -631,9 +630,8 @@ public class FileUploader extends Service
 
                     newUpload = new UploadFileOperation(
                             account,
-                            files[i],
+                            file,
                             ocUpload,
-                            chunked,
                             forceOverwrite,
                             localAction,
                             this,
@@ -651,7 +649,7 @@ public class FileUploader extends Service
 
                     Pair<String, String> putResult = mPendingUploads.putIfAbsent(
                             account.name,
-                            files[i].getRemotePath(),
+                            file.getRemotePath(),
                             newUpload
                     );
                     if (putResult != null) {
@@ -692,7 +690,6 @@ public class FileUploader extends Service
                     account,
                     null,
                     upload,
-                    chunked,
                     upload.isForceOverwrite(),  // TODO should be read from DB?
                     upload.getLocalAction(),    // TODO should be read from DB?
                     this,
@@ -739,7 +736,7 @@ public class FileUploader extends Service
      * uploads.
      */
     @Override
-    public IBinder onBind(Intent arg0) {
+    public IBinder onBind(Intent intent) {
         return mBinder;
     }
 
@@ -868,17 +865,17 @@ public class FileUploader extends Service
             if (account == null || file == null) {
                 return false;
             }
-            return (mPendingUploads.contains(account.name, file.getRemotePath()));
+            return mPendingUploads.contains(account.name, file.getRemotePath());
         }
 
         public boolean isUploadingNow(OCUpload upload) {
-            return (
-                upload != null  &&
-                mCurrentAccount != null &&
-                mCurrentUpload != null &&
-                upload.getAccountName().equals(mCurrentAccount.name) &&
-                upload.getRemotePath().equals(mCurrentUpload.getRemotePath())
-            );
+            return
+                    upload != null &&
+                            mCurrentAccount != null &&
+                            mCurrentUpload != null &&
+                            upload.getAccountName().equals(mCurrentAccount.name) &&
+                            upload.getRemotePath().equals(mCurrentUpload.getRemotePath())
+            ;
         }
 
         /**
@@ -1156,7 +1153,7 @@ public class FileUploader extends Service
                         String.format(getString(R.string.uploader_upload_in_progress_content), 0, upload.getFileName())
                 );
 
-        if ((android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O)) {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
             mNotificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD);
         }
 
@@ -1226,7 +1223,7 @@ public class FileUploader extends Service
             String content;
 
             // check credentials error
-            boolean needsToUpdateCredentials = (ResultCode.UNAUTHORIZED.equals(uploadResult.getCode()));
+            boolean needsToUpdateCredentials = ResultCode.UNAUTHORIZED.equals(uploadResult.getCode());
             tickerId = (needsToUpdateCredentials) ?
                     R.string.uploader_upload_failed_credentials_error : tickerId;
 

+ 9 - 10
src/main/java/com/owncloud/android/files/services/IndexedForest.java

@@ -45,10 +45,10 @@ public class IndexedForest<V> {
 
     @SuppressWarnings("PMD.ShortClassName")
     private class Node<V> {
-        private String mKey = null;
-        private Node<V> mParent = null;
+        private String mKey;
+        private Node<V> mParent;
         private Set<Node<V>> mChildren = new HashSet<>();    // TODO be careful with hash()
-        private V mPayload = null;
+        private V mPayload;
 
         // payload is optional
         public Node(String key, V payload) {
@@ -113,8 +113,8 @@ public class IndexedForest<V> {
         } else {
             // value really added
             String currentPath = remotePath;
-            String parentPath = null;
-            String parentKey = null;
+            String parentPath;
+            String parentKey;
             Node<V> currentNode = valuedNode;
             Node<V> parentNode = null;
             boolean linked = false;
@@ -142,7 +142,7 @@ public class IndexedForest<V> {
                 linkedTo = parentNode.getKey().substring(accountName.length());
             }
 
-            return new Pair<String, String>(targetKey, linkedTo);
+            return new Pair<>(targetKey, linkedTo);
         }
     }
 
@@ -156,7 +156,7 @@ public class IndexedForest<V> {
                 return remove(accountName, remotePath);
             }
         }
-        return new Pair<V, String>(null, null);
+        return new Pair<>(null, null);
     }
 
 
@@ -186,10 +186,10 @@ public class IndexedForest<V> {
                 unlinkedFrom = parent.getKey().substring(accountName.length());
             }
 
-            return new Pair<V, String>(firstRemoved.getPayload(), unlinkedFrom);
+            return new Pair<>(firstRemoved.getPayload(), unlinkedFrom);
         }
 
-        return new Pair<V, String>(null, null);
+        return new Pair<>(null, null);
     }
 
     private void removeDescendants(Node<V> removed) {
@@ -246,5 +246,4 @@ public class IndexedForest<V> {
     private String buildKey(String accountName, String remotePath) {
         return accountName + remotePath;
     }
-
 }

+ 57 - 50
src/main/java/com/owncloud/android/jobs/ContactsBackupJob.java

@@ -110,65 +110,60 @@ public class ContactsBackupJob extends Job {
 
     private void backupContact(Account account, String backupFolder) {
         ArrayList<String> vCard = new ArrayList<>();
-        try {
 
-            Cursor cursor = getContext().getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null,
-                    null, null, null);
+        Cursor cursor = getContext().getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null,
+                null, null, null);
 
-            if (cursor != null && cursor.getCount() > 0) {
-                cursor.moveToFirst();
-                for (int i = 0; i < cursor.getCount(); i++) {
+        if (cursor != null && cursor.getCount() > 0) {
+            cursor.moveToFirst();
+            for (int i = 0; i < cursor.getCount(); i++) {
 
-                    vCard.add(getContactFromCursor(cursor));
-                    cursor.moveToNext();
-                }
+                vCard.add(getContactFromCursor(cursor));
+                cursor.moveToNext();
             }
+        }
 
-            String filename = DateFormat.format("yyyy-MM-dd_HH-mm-ss", Calendar.getInstance()).toString() + ".vcf";
-            Log_OC.d(TAG, "Storing: " + filename);
-            File file = new File(getContext().getCacheDir(), filename);
+        String filename = DateFormat.format("yyyy-MM-dd_HH-mm-ss", Calendar.getInstance()).toString() + ".vcf";
+        Log_OC.d(TAG, "Storing: " + filename);
+        File file = new File(getContext().getCacheDir(), filename);
 
-            FileWriter fw = null;
-            try {
-                fw = new FileWriter(file);
+        FileWriter fw = null;
+        try {
+            fw = new FileWriter(file);
 
-                for (String card : vCard) {
-                    fw.write(card);
-                }
+            for (String card : vCard) {
+                fw.write(card);
+            }
 
-            } catch (IOException e) {
-                Log_OC.d(TAG, "Error ", e);
-            } finally {
-                if (cursor != null) {
-                    cursor.close();
-                }
-                
-                if (fw != null) {
-                    try {
-                        fw.close();
-                    } catch (IOException e) {
-                        Log_OC.d(TAG, "Error closing file writer ", e);
-                    }
-                }
+        } catch (IOException e) {
+            Log_OC.d(TAG, "Error ", e);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
             }
 
-            FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-            requester.uploadNewFile(
-                    getContext(),
-                    account,
-                    file.getAbsolutePath(),
-                    backupFolder + filename,
-                    FileUploader.LOCAL_BEHAVIOUR_MOVE,
-                    null,
-                    true,
-                    UploadFileOperation.CREATED_BY_USER,
-                    false,
-                    false
-            );
-
-        } catch (Exception e) {
-            Log_OC.d(TAG, e.getMessage());
+            if (fw != null) {
+                try {
+                    fw.close();
+                } catch (IOException e) {
+                    Log_OC.d(TAG, "Error closing file writer ", e);
+                }
+            }
         }
+
+        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
+        requester.uploadNewFile(
+                getContext(),
+                account,
+                file.getAbsolutePath(),
+                backupFolder + filename,
+                FileUploader.LOCAL_BEHAVIOUR_MOVE,
+                null,
+                true,
+                UploadFileOperation.CREATED_BY_USER,
+                false,
+                false
+        );
     }
 
     private void expireFiles(Integer daysToExpire, String backupFolderString, Account account) {
@@ -209,9 +204,10 @@ public class ContactsBackupJob extends Job {
         Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey);
 
         String vCard = "";
+        InputStream inputStream = null;
+        InputStreamReader inputStreamReader = null;
         try {
-            InputStream inputStream = getContext().getContentResolver().openInputStream(uri);
-            InputStreamReader inputStreamReader;
+            inputStream = getContext().getContentResolver().openInputStream(uri);
             char[] buffer = new char[1024];
             StringBuilder stringBuilder = new StringBuilder();
 
@@ -235,6 +231,17 @@ public class ContactsBackupJob extends Job {
 
         } catch (IOException e) {
             Log_OC.d(TAG, e.getMessage());
+        } finally {
+            try {
+                if (inputStream != null) {
+                    inputStream.close();
+                }
+                if (inputStreamReader != null) {
+                    inputStreamReader.close();
+                }
+            } catch (IOException e) {
+                Log_OC.e(TAG, "failed to close stream");
+            }
         }
         return vCard;
     }

+ 2 - 3
src/main/java/com/owncloud/android/jobs/FilesSyncJob.java

@@ -51,7 +51,6 @@ import com.owncloud.android.utils.MimeTypeUtil;
 import com.owncloud.android.utils.PowerUtils;
 
 import java.io.File;
-import java.io.IOException;
 import java.text.ParsePosition;
 import java.text.SimpleDateFormat;
 import java.util.Date;
@@ -72,7 +71,7 @@ public class FilesSyncJob extends Job {
 
     @NonNull
     @Override
-    protected Result onRunJob(Params params) {
+    protected Result onRunJob(@NonNull Params params) {
         final Context context = MainApp.getAppContext();
         final ContentResolver contentResolver = context.getContentResolver();
         FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
@@ -129,7 +128,7 @@ public class FilesSyncJob extends Job {
                                     lastModificationTime = dateTime.getTime();
                                 }
 
-                            } catch (IOException e) {
+                            } catch (Exception e) {
                                 Log_OC.d(TAG, "Failed to get the proper time " + e.getLocalizedMessage());
                             }
                         }

+ 173 - 0
src/main/java/com/owncloud/android/jobs/MediaFoldersDetectionJob.java

@@ -0,0 +1,173 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2018 Mario Danic
+ * Copyright (C) 2018 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.jobs;
+
+
+import android.accounts.Account;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.media.RingtoneManager;
+import android.support.annotation.NonNull;
+import android.support.v4.app.NotificationCompat;
+import android.text.TextUtils;
+
+import com.evernote.android.job.Job;
+import com.google.gson.Gson;
+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.MediaFoldersModel;
+import com.owncloud.android.datamodel.MediaProvider;
+import com.owncloud.android.datamodel.SyncedFolderProvider;
+import com.owncloud.android.ui.activity.ManageAccountsActivity;
+import com.owncloud.android.ui.activity.SyncedFoldersActivity;
+import com.owncloud.android.ui.notifications.NotificationUtils;
+import com.owncloud.android.utils.ThemeUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MediaFoldersDetectionJob extends Job {
+    public static final String TAG = "MediaFoldersDetectionJob";
+
+    public static final String KEY_MEDIA_FOLDER_PATH = "KEY_MEDIA_FOLDER_PATH";
+    public static final String KEY_MEDIA_FOLDER_TYPE = "KEY_MEDIA_FOLDER_TYPE";
+
+    private static final String ACCOUNT_NAME_GLOBAL = "global";
+    private static final String KEY_MEDIA_FOLDERS = "media_folders";
+
+    @NonNull
+    @Override
+    protected Result onRunJob(@NonNull Params params) {
+        Context context = getContext();
+        ContentResolver contentResolver = context.getContentResolver();
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(contentResolver);
+        SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(contentResolver);
+        Gson gson = new Gson();
+        String arbitraryDataString;
+        MediaFoldersModel mediaFoldersModel;
+
+        List<MediaFolder> imageMediaFolders = MediaProvider.getImageFolders(contentResolver, 1,
+                null, true);
+        List<MediaFolder> videoMediaFolders = MediaProvider.getVideoFolders(contentResolver, 1, null,
+                true);
+
+        List<String> imageMediaFolderPaths = new ArrayList<>();
+        List<String> videoMediaFolderPaths = new ArrayList<>();
+
+        for (MediaFolder imageMediaFolder: imageMediaFolders) {
+            imageMediaFolderPaths.add(imageMediaFolder.absolutePath);
+        }
+
+        for (MediaFolder videoMediaFolder: videoMediaFolders) {
+            imageMediaFolderPaths.add(videoMediaFolder.absolutePath);
+        }
+
+        arbitraryDataString = arbitraryDataProvider.getValue(ACCOUNT_NAME_GLOBAL, KEY_MEDIA_FOLDERS);
+        if (!TextUtils.isEmpty(arbitraryDataString)) {
+            mediaFoldersModel = gson.fromJson(arbitraryDataString, MediaFoldersModel.class);
+
+            // Store updated values
+            arbitraryDataProvider.storeOrUpdateKeyValue(ACCOUNT_NAME_GLOBAL, KEY_MEDIA_FOLDERS, gson.toJson(new
+                    MediaFoldersModel(imageMediaFolderPaths, videoMediaFolderPaths)));
+
+            imageMediaFolderPaths.removeAll(mediaFoldersModel.getImageMediaFolders());
+            videoMediaFolderPaths.removeAll(mediaFoldersModel.getVideoMediaFolders());
+
+            if (!imageMediaFolderPaths.isEmpty() || !videoMediaFolderPaths.isEmpty()) {
+                Account[] accounts = AccountUtils.getAccounts(getContext());
+                List<Account> accountList = new ArrayList<>();
+                for (Account account : accounts) {
+                    if (!arbitraryDataProvider.getBooleanValue(account, ManageAccountsActivity.PENDING_FOR_REMOVAL)) {
+                        accountList.add(account);
+                    }
+                }
+
+                for (Account account : accountList) {
+                    for (String imageMediaFolder : imageMediaFolderPaths) {
+                        if (syncedFolderProvider.findByLocalPathAndAccount(imageMediaFolder,
+                                account) == null) {
+                            sendNotification(String.format(context.getString(R.string.new_media_folder_detected),
+                                    context.getString(R.string.new_media_folder_photos)),
+                                    imageMediaFolder.substring(imageMediaFolder.lastIndexOf('/') + 1,
+                                            imageMediaFolder.length()),
+                                    account, imageMediaFolder, 1);
+                        }
+                    }
+
+                    for (String videoMediaFolder : videoMediaFolderPaths) {
+                        if (syncedFolderProvider.findByLocalPathAndAccount(videoMediaFolder,
+                                account) == null) {
+                            sendNotification(String.format(context.getString(R.string.new_media_folder_detected),
+                                    context.getString(R.string.new_media_folder_videos)),
+                                    videoMediaFolder.substring(videoMediaFolder.lastIndexOf('/') + 1,
+                                            videoMediaFolder.length()),
+                                    account, videoMediaFolder, 2);
+                        }
+                    }
+                }
+            }
+
+        } else {
+            mediaFoldersModel = new MediaFoldersModel(imageMediaFolderPaths, videoMediaFolderPaths);
+            arbitraryDataProvider.storeOrUpdateKeyValue(ACCOUNT_NAME_GLOBAL, KEY_MEDIA_FOLDERS, gson.toJson(mediaFoldersModel));
+        }
+
+        return Result.SUCCESS;
+    }
+
+    private void sendNotification(String contentTitle, String subtitle,  Account account,
+                                  String path, int type) {
+        Context context = getContext();
+        Intent intent = new Intent(getContext(), SyncedFoldersActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        intent.putExtra(NotificationJob.KEY_NOTIFICATION_ACCOUNT, account.name);
+        intent.putExtra(KEY_MEDIA_FOLDER_PATH, path);
+        intent.putExtra(KEY_MEDIA_FOLDER_TYPE, type);
+        intent.putExtra(SyncedFoldersActivity.EXTRA_SHOW_SIDEBAR, true);
+        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT);
+
+        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(
+                context, NotificationUtils.NOTIFICATION_CHANNEL_GENERAL)
+                .setSmallIcon(R.drawable.notification_icon)
+                .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.notification_icon))
+                .setColor(ThemeUtils.primaryColor(getContext()))
+                .setSubText(account.name)
+                .setContentTitle(contentTitle)
+                .setContentText(subtitle)
+                .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
+                .setAutoCancel(true)
+                .setContentIntent(pendingIntent);
+
+        NotificationManager notificationManager = (NotificationManager)
+                context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+        if (notificationManager != null) {
+            notificationManager.notify(0, notificationBuilder.build());
+        }
+    }
+}

+ 2 - 0
src/main/java/com/owncloud/android/jobs/NCJobCreator.java

@@ -43,6 +43,8 @@ public class NCJobCreator implements JobCreator {
                 return new OfflineSyncJob();
             case NotificationJob.TAG:
                 return new NotificationJob();
+            case MediaFoldersDetectionJob.TAG:
+                return new MediaFoldersDetectionJob();
             default:
                 return null;
         }

+ 23 - 8
src/main/java/com/owncloud/android/jobs/NContentObserverJob.java

@@ -1,4 +1,4 @@
-/**
+/*
  * Nextcloud Android client application
  *
  * @author Mario Danic
@@ -28,11 +28,13 @@ import android.support.annotation.RequiresApi;
 
 import com.evernote.android.job.JobRequest;
 import com.evernote.android.job.util.support.PersistableBundleCompat;
+import com.owncloud.android.datamodel.SyncedFolderProvider;
 import com.owncloud.android.utils.FilesSyncHelper;
 import com.owncloud.android.utils.PowerUtils;
 
 /*
     Job that triggers new FilesSyncJob in case new photo or video were detected
+    and starts a job to find new media folders
  */
 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
 public class NContentObserverJob extends JobService {
@@ -42,26 +44,39 @@ public class NContentObserverJob extends JobService {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
             if (params.getJobId() == FilesSyncHelper.ContentSyncJobId && params.getTriggeredContentAuthorities()
                     != null && params.getTriggeredContentUris() != null
-                    && params.getTriggeredContentUris().length > 0
-                    && !PowerUtils.isPowerSaveMode(getApplicationContext())) {
+                    && params.getTriggeredContentUris().length > 0) {
 
-                PersistableBundleCompat persistableBundleCompat = new PersistableBundleCompat();
-                persistableBundleCompat.putBoolean(FilesSyncJob.SKIP_CUSTOM, true);
+                checkAndStartFileSyncJob();
 
-                new JobRequest.Builder(FilesSyncJob.TAG)
+                new JobRequest.Builder(MediaFoldersDetectionJob.TAG)
                         .startNow()
-                        .setExtras(persistableBundleCompat)
                         .setUpdateCurrent(false)
                         .build()
                         .schedule();
+
             }
 
-            FilesSyncHelper.scheduleNJobs(true, getApplicationContext());
+            FilesSyncHelper.scheduleJobOnN();
         }
 
         return true;
     }
 
+    private void checkAndStartFileSyncJob() {
+        if (!PowerUtils.isPowerSaveMode(getApplicationContext()) &&
+                new SyncedFolderProvider(getContentResolver()).countEnabledSyncedFolders() > 0) {
+            PersistableBundleCompat persistableBundleCompat = new PersistableBundleCompat();
+            persistableBundleCompat.putBoolean(FilesSyncJob.SKIP_CUSTOM, true);
+
+            new JobRequest.Builder(FilesSyncJob.TAG)
+                    .startNow()
+                    .setExtras(persistableBundleCompat)
+                    .setUpdateCurrent(false)
+                    .build()
+                    .schedule();
+        }
+    }
+
     @Override
     public boolean onStopJob(JobParameters params) {
         return false;

+ 5 - 11
src/main/java/com/owncloud/android/jobs/NotificationJob.java

@@ -60,7 +60,7 @@ public class NotificationJob extends Job {
 
     @NonNull
     @Override
-    protected Result onRunJob(Params params) {
+    protected Result onRunJob(@NonNull Params params) {
 
         Context context = getContext();
         PersistableBundleCompat persistableBundleCompat = getParams().getExtras();
@@ -68,7 +68,6 @@ public class NotificationJob extends Job {
         String signature = persistableBundleCompat.getString(KEY_NOTIFICATION_SIGNATURE, "");
 
         if (!TextUtils.isEmpty(subject) && !TextUtils.isEmpty(signature)) {
-
             try {
                 byte[] base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT);
                 byte[] base64DecodedSignature = Base64.decode(signature, Base64.DEFAULT);
@@ -92,14 +91,10 @@ public class NotificationJob extends Job {
                         if (!decryptedPushMessage.getApp().equals("spreed")) {
                             sendNotification(decryptedPushMessage.getSubject(), signatureVerification.getAccount());
                         }
-
                     }
-                } catch (NoSuchAlgorithmException e1) {
-                    Log.d(TAG, "No proper algorithm to decrypt the message " + e1.getLocalizedMessage());
-                } catch (NoSuchPaddingException e1) {
-                    Log.d(TAG, "No proper padding to decrypt the message " + e1.getLocalizedMessage());
-                } catch (InvalidKeyException e1) {
-                    Log.d(TAG, "Invalid private key " + e1.getLocalizedMessage());
+                } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e1) {
+                    Log.d(TAG, "Error decrypting message " + e1.getClass().getName()
+                            + " " + e1.getLocalizedMessage());
                 }
             } catch (Exception exception) {
                 Log.d(TAG, "Something went very wrong" + exception.getLocalizedMessage());
@@ -127,7 +122,7 @@ public class NotificationJob extends Job {
                 .setAutoCancel(true)
                 .setContentIntent(pendingIntent);
 
-        if ((android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O)) {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
             notificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_PUSH);
         }
 
@@ -136,5 +131,4 @@ public class NotificationJob extends Job {
 
         notificationManager.notify(0, notificationBuilder.build());
     }
-
 }

+ 8 - 115
src/main/java/com/owncloud/android/media/MediaControlView.java

@@ -49,13 +49,11 @@ import java.util.Locale;
 /**
  * View containing controls for a {@link MediaPlayer}. 
  *
- * Holds buttons "play / pause", "rewind", "fast forward" 
- * and a progress slider. 
+ * Holds buttons "play / pause", "rewind", "fast forward" and a progress slider. 
  *
- * It synchronizes itself with the state of the 
- * {@link MediaPlayer}.
+ * It synchronizes itself with the state of the {@link MediaPlayer}.
  */
-public class MediaControlView extends FrameLayout /* implements OnLayoutChangeListener, OnTouchListener */ implements OnClickListener, OnSeekBarChangeListener {
+public class MediaControlView extends FrameLayout implements OnClickListener, OnSeekBarChangeListener {
     private static final String TAG = MediaControlView.class.getSimpleName();
 
     private MediaPlayerControl mPlayer;
@@ -89,107 +87,8 @@ public class MediaControlView extends FrameLayout /* implements OnLayoutChangeLi
     @Override
     public void onFinishInflate() {
         super.onFinishInflate();
-        /*
-        if (mRoot != null)
-            initControllerView(mRoot);
-         */
     }
 
-    /* TODO REMOVE
-    public MediaControlView(Context context, boolean useFastForward) {
-        super(context);
-        mContext = context;
-        mUseFastForward = useFastForward;
-        initFloatingWindowLayout();
-        //initFloatingWindow();
-    }
-    */
-
-    /* TODO REMOVE
-    public MediaControlView(Context context) {
-        this(context, true);
-    }
-    */
-    
-    /* T
-    private void initFloatingWindow() {
-        mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
-        mWindow = PolicyManager.makeNewWindow(mContext);
-        mWindow.setWindowManager(mWindowManager, null, null);
-        mWindow.requestFeature(Window.FEATURE_NO_TITLE);
-        mDecor = mWindow.getDecorView();
-        mDecor.setOnTouchListener(mTouchListener);
-        mWindow.setContentView(this);
-        mWindow.setBackgroundDrawableResource(android.R.color.transparent);
-        
-        // While the media controller is up, the volume control keys should
-        // affect the media stream type
-        mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC);
-
-        setFocusable(true);
-        setFocusableInTouchMode(true);
-        setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
-        requestFocus();
-    }
-    */
-
-    /*
-    // Allocate and initialize the static parts of mDecorLayoutParams. Must
-    // also call updateFloatingWindowLayout() to fill in the dynamic parts
-    // (y and width) before mDecorLayoutParams can be used.
-    private void initFloatingWindowLayout() {
-        mDecorLayoutParams = new WindowManager.LayoutParams();
-        WindowManager.LayoutParams p = mDecorLayoutParams;
-        p.gravity = Gravity.TOP;
-        p.height = LayoutParams.WRAP_CONTENT;
-        p.x = 0;
-        p.format = PixelFormat.TRANSLUCENT;
-        p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
-        p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
-                | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
-                | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
-        p.token = null;
-        p.windowAnimations = 0; // android.R.style.DropDownAnimationDown;
-    }
-    */
-
-    // Update the dynamic parts of mDecorLayoutParams
-    // Must be called with mAnchor != NULL.
-    /*
-    private void updateFloatingWindowLayout() {
-        int [] anchorPos = new int[2];
-        mAnchor.getLocationOnScreen(anchorPos);
-
-        WindowManager.LayoutParams p = mDecorLayoutParams;
-        p.width = mAnchor.getWidth();
-        p.y = anchorPos[1] + mAnchor.getHeight();
-    }
-    */
-
-    /*
-    // This is called whenever mAnchor's layout bound changes
-    public void onLayoutChange(View v, int left, int top, int right,
-            int bottom, int oldLeft, int oldTop, int oldRight,
-            int oldBottom) {
-        //updateFloatingWindowLayout();
-        if (mShowing) {
-            mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams);
-        }
-    }
-    */
-    
-    /*
-    public boolean onTouch(View v, MotionEvent event) {
-        if (event.getAction() == MotionEvent.ACTION_DOWN) {
-            if (mShowing) {
-                hide();
-            }
-        }
-            return false;
-    }
-    */
-
-
     public void setMediaPlayer(MediaPlayerControl player) {
         mPlayer = player;
         mHandler.sendEmptyMessage(SHOW_PROGRESS);
@@ -335,7 +234,6 @@ public class MediaControlView extends FrameLayout /* implements OnLayoutChangeLi
             if (uniqueDown && !mPlayer.isPlaying()) {
                 mPlayer.start();
                 updatePausePlay();
-                //show(sDefaultTimeout);
             }
             return true;
         } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
@@ -343,12 +241,10 @@ public class MediaControlView extends FrameLayout /* implements OnLayoutChangeLi
             if (uniqueDown && mPlayer.isPlaying()) {
                 mPlayer.pause();
                 updatePausePlay();
-                //show(sDefaultTimeout);
             }
             return true;
         }
 
-        //show(sDefaultTimeout);
         return super.dispatchKeyEvent(event);
     }
 
@@ -433,16 +329,15 @@ public class MediaControlView extends FrameLayout /* implements OnLayoutChangeLi
         }
 
         long duration = mPlayer.getDuration();
-        long newposition = (duration * progress) / 1000L;
-        mPlayer.seekTo((int) newposition);
+        long newPosition = (duration * progress) / 1000L;
+        mPlayer.seekTo((int) newPosition);
         if (mCurrentTime != null) {
-            mCurrentTime.setText(stringForTime((int) newposition));
+            mCurrentTime.setText(stringForTime((int) newPosition));
         }
     }
 
     /**
-     * Called in devices with touchpad when the user starts to adjust the 
-     * position of the seekbar's thumb.
+     * Called in devices with touchpad when the user starts to adjust the position of the seekbar's thumb.
      *
      * Will be followed by several onProgressChanged notifications.
      */
@@ -454,8 +349,7 @@ public class MediaControlView extends FrameLayout /* implements OnLayoutChangeLi
 
 
     /**
-     * Called in devices with touchpad when the user finishes the
-     * adjusting of the seekbar.
+     * Called in devices with touchpad when the user finishes the adjusting of the seekbar.
      */
     @Override
     public void onStopTrackingTouch(SeekBar seekBar) {
@@ -476,5 +370,4 @@ public class MediaControlView extends FrameLayout /* implements OnLayoutChangeLi
         super.onInitializeAccessibilityNodeInfo(info);
         info.setClassName(MediaControlView.class.getName());
     }
-
 }

+ 81 - 49
src/main/java/com/owncloud/android/media/MediaService.java

@@ -1,9 +1,13 @@
-/**
+/*
  * ownCloud Android client application
  *
  * @author David A. Velasco
  * Copyright (C) 2016 ownCloud Inc.
  *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2018 Tobias Kaminsky
+ * Copyright (C) 2018 Nextcloud 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.
@@ -20,6 +24,8 @@
 package com.owncloud.android.media;
 
 import android.accounts.Account;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
@@ -32,6 +38,7 @@ import android.media.MediaPlayer.OnErrorListener;
 import android.media.MediaPlayer.OnPreparedListener;
 import android.net.wifi.WifiManager;
 import android.net.wifi.WifiManager.WifiLock;
+import android.os.AsyncTask;
 import android.os.IBinder;
 import android.os.PowerManager;
 import android.support.v4.app.NotificationCompat;
@@ -39,6 +46,12 @@ import android.widget.Toast;
 
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.files.StreamMediaFileOperation;
+import com.owncloud.android.lib.common.OwnCloudAccount;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
+import com.owncloud.android.lib.common.accounts.AccountUtils;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.ui.activity.FileActivity;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
@@ -46,6 +59,7 @@ import com.owncloud.android.ui.notifications.NotificationUtils;
 import com.owncloud.android.utils.ThemeUtils;
 
 import java.io.IOException;
+import java.lang.ref.WeakReference;
 
 
 /**
@@ -365,7 +379,6 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
         }
     }
 
-
     /**
      * Fully releases the audio focus.
      */
@@ -443,59 +456,34 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
 
             createMediaPlayerIfNeeded();
             mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
-            String url = mFile.getStoragePath();
 
-            /* Streaming is not possible right now
-            if (url == null || url.length() <= 0) {
-                url = AccountUtils.constructFullURLForAccount(this, mAccount) + mFile.getRemotePath();
-            }
-            mIsStreaming = url.startsWith("http:") || url.startsWith("https:");
-            */
-            //mIsStreaming = false;
-            mPlayer.setDataSource(url);
-
-            mState = State.PREPARING;
-            setUpAsForeground(String.format(getString(R.string.media_state_loading), mFile.getFileName()));
-
-            // starts preparing the media player in background
-            mPlayer.prepareAsync();
+            if (mFile.isDown()) {
+                mPlayer.setDataSource(mFile.getStoragePath());
+                preparePlayer();
+            } else {
+                OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount, getBaseContext());
+                OwnCloudClient client = OwnCloudClientManagerFactory.getDefaultSingleton().
+                        getClientFor(ocAccount, getBaseContext());
 
-            // prevent the Wifi from going to sleep when streaming
-            /*
-            if (mIsStreaming) {
-                mWifiLock.acquire();
-            } else
-            */
-            if (mWifiLock.isHeld()) {
-                mWifiLock.release();
+                new LoadStreamUrl(this, client).execute(mFile.getLocalId());
             }
-
-        } catch (SecurityException e) {
-            Log_OC.e(TAG, "SecurityException playing " + mAccount.name + mFile.getRemotePath(), e);
-            Toast.makeText(this, String.format(getString(R.string.media_err_security_ex), mFile.getFileName()),
-                    Toast.LENGTH_LONG).show();
-            processStopRequest(true);
-
-        } catch (IOException e) {
-            Log_OC.e(TAG, "IOException playing " + mAccount.name + mFile.getRemotePath(), e);
-            Toast.makeText(this, String.format(getString(R.string.media_err_io_ex), mFile.getFileName()),
-                    Toast.LENGTH_LONG).show();
-            processStopRequest(true);
-
-        } catch (IllegalStateException e) {
-            Log_OC.e(TAG, "IllegalStateException " + mAccount.name + mFile.getRemotePath(), e);
-            Toast.makeText(this, String.format(getString(R.string.media_err_unexpected), mFile.getFileName()),
-                    Toast.LENGTH_LONG).show();
-            processStopRequest(true);
-
-        } catch (IllegalArgumentException e) {
-            Log_OC.e(TAG, "IllegalArgumentException " + mAccount.name + mFile.getRemotePath(), e);
-            Toast.makeText(this, String.format(getString(R.string.media_err_unexpected), mFile.getFileName()),
+        } catch (AccountUtils.AccountNotFoundException | OperationCanceledException | AuthenticatorException e) {
+            Log_OC.e(TAG, "Loading stream url not possible: " + e.getMessage());
+        } catch (SecurityException | IOException | IllegalStateException | IllegalArgumentException e) {
+            Log_OC.e(TAG, e.getClass().getSimpleName() + " playing " + mAccount.name + mFile.getRemotePath(), e);
+            Toast.makeText(this, String.format(getString(R.string.media_err_playing), mFile.getFileName()),
                     Toast.LENGTH_LONG).show();
             processStopRequest(true);
         }
     }
 
+    private void preparePlayer() {
+        mState = State.PREPARING;
+        setUpAsForeground(String.format(getString(R.string.media_state_loading), mFile.getFileName()));
+
+        // starts preparing the media player in background
+        mPlayer.prepareAsync();
+    }
 
     /** Called when media player is done playing current song. */
     public void onCompletion(MediaPlayer player) {
@@ -556,7 +544,7 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
         mNotificationBuilder.setContentTitle(ticker);
         mNotificationBuilder.setContentText(content);
 
-        if ((android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O)) {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
             mNotificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_MEDIA);
         }
 
@@ -593,7 +581,7 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
         mNotificationBuilder.setContentTitle(ticker);
         mNotificationBuilder.setContentText(content);
 
-        if ((android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O)) {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
             mNotificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_MEDIA);
         }
 
@@ -720,4 +708,48 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
         return mMediaController;
     }
 
+    private static class LoadStreamUrl extends AsyncTask<String, Void, String> {
+
+        private OwnCloudClient client;
+        private WeakReference<MediaService> mediaServiceWeakReference;
+
+        public LoadStreamUrl(MediaService mediaService, OwnCloudClient client) {
+            this.client = client;
+            this.mediaServiceWeakReference = new WeakReference<>(mediaService);
+        }
+
+        @Override
+        protected String doInBackground(String... fileId) {
+            StreamMediaFileOperation sfo = new StreamMediaFileOperation(fileId[0]);
+            RemoteOperationResult result = sfo.execute(client);
+
+            if (!result.isSuccess()) {
+                return null;
+            }
+
+            return (String) result.getData().get(0);
+        }
+
+        @Override
+        protected void onPostExecute(String url) {
+            MediaService mediaService = mediaServiceWeakReference.get();
+
+            if (mediaService != null) {
+                if (url != null) {
+                    try {
+                        mediaService.mPlayer.setDataSource(url);
+
+                        // prevent the Wifi from going to sleep when streaming
+                        mediaService.mWifiLock.acquire();
+                        mediaService.preparePlayer();
+                    } catch (IOException e) {
+                        Log_OC.e(TAG, "Streaming not possible: " + e.getMessage());
+                    }
+                } else {
+                    // we already show a toast with error from media player
+                    mediaService.processStopRequest(true);
+                }
+            }
+        }
+    }
 }

+ 2 - 4
src/main/java/com/owncloud/android/media/MediaServiceBinder.java

@@ -43,7 +43,7 @@ public class MediaServiceBinder extends Binder implements MediaController.MediaP
     /**
      * {@link MediaService} instance to access with the binder
      */
-    private MediaService mService = null;
+    private MediaService mService;
 
     /**
      * Public constructor
@@ -57,12 +57,10 @@ public class MediaServiceBinder extends Binder implements MediaController.MediaP
         mService = service;
     }
 
-
     public boolean isPlaying(OCFile mFile) {
-        return (mFile != null && mFile.equals(mService.getCurrentFile()));
+        return mFile != null && mFile.equals(mService.getCurrentFile());
     }
 
-
     @Override
     public boolean canPause() {
         return true;

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

@@ -36,7 +36,7 @@ import java.util.ArrayList;
  */
 public class CheckCurrentCredentialsOperation extends SyncOperation {
 
-    private Account mAccount = null;
+    private Account mAccount;
 
     public CheckCurrentCredentialsOperation(Account account) {
         if (account == null) {
@@ -55,7 +55,7 @@ public class CheckCurrentCredentialsOperation extends SyncOperation {
         } else {
             RemoteOperation check = new ExistenceCheckRemoteOperation(OCFile.ROOT_PATH, false);
             result = check.execute(client);
-            ArrayList<Object> data = new ArrayList<Object>();
+            ArrayList<Object> data = new ArrayList<>();
             data.add(mAccount);
             result.setData(data);
         }

+ 112 - 0
src/main/java/com/owncloud/android/operations/CommentFileOperation.java

@@ -0,0 +1,112 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2018 Tobias Kaminsky
+ * Copyright (C) 2018 Nextcloud GmbH.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU 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 <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.operations;
+
+import android.util.Log;
+
+import com.google.gson.GsonBuilder;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.operations.common.SyncOperation;
+
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.StringRequestEntity;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * Restore a {@link com.owncloud.android.lib.resources.files.FileVersion}.
+ */
+public class CommentFileOperation extends SyncOperation {
+
+    private static final String TAG = CommentFileOperation.class.getSimpleName();
+    private static final int POST_READ_TIMEOUT = 30000;
+    private static final int POST_CONNECTION_TIMEOUT = 5000;
+
+    private static final String ACTOR_ID = "actorId";
+    private static final String ACTOR_TYPE = "actorType";
+    private static final String ACTOR_TYPE_VALUE = "users";
+    private static final String VERB = "verb";
+    private static final String VERB_VALUE = "comment";
+    private static final String MESSAGE = "message";
+
+    private String message;
+    private String fileId;
+    private String userId;
+
+    /**
+     * Constructor
+     *
+     * @param message Comment to store
+     * @param userId  userId to access correct dav endpoint
+     */
+    public CommentFileOperation(String message, String fileId, String userId) {
+        this.message = message;
+        this.fileId = fileId;
+        this.userId = userId;
+    }
+
+    /**
+     * Performs the operation.
+     *
+     * @param client Client object to communicate with the remote ownCloud server.
+     */
+    @Override
+    protected RemoteOperationResult run(OwnCloudClient client) {
+
+        RemoteOperationResult result;
+        try {
+            String url = client.getNewWebdavUri() + "/comments/files/" + fileId;
+            PostMethod postMethod = new PostMethod(url);
+            postMethod.addRequestHeader("Content-type", "application/json");
+
+            Map<String, String> values = new HashMap<>();
+            values.put(ACTOR_ID, userId);
+            values.put(ACTOR_TYPE, ACTOR_TYPE_VALUE);
+            values.put(VERB, VERB_VALUE);
+            values.put(MESSAGE, message);
+
+            String json = new GsonBuilder().create().toJson(values, Map.class);
+
+            postMethod.setRequestEntity(new StringRequestEntity(json));
+
+            int status = client.executeMethod(postMethod, POST_READ_TIMEOUT, POST_CONNECTION_TIMEOUT);
+
+            result = new RemoteOperationResult(isSuccess(status), postMethod);
+
+            client.exhaustResponse(postMethod.getResponseBodyAsStream());
+        } catch (IOException e) {
+            result = new RemoteOperationResult(e);
+            Log.e(TAG, "Post comment to file with id " + fileId + " failed: " + result.getLogMessage(), e);
+        }
+
+        return result;
+    }
+
+    private boolean isSuccess(int status) {
+        return status == HttpStatus.SC_CREATED;
+    }
+}

+ 3 - 3
src/main/java/com/owncloud/android/operations/DownloadFileOperation.java

@@ -56,8 +56,8 @@ public class DownloadFileOperation extends RemoteOperation {
     private OCFile mFile;
     private String mBehaviour;
     private Context mContext;
-    private Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<OnDatatransferProgressListener>();
-    private long mModificationTimestamp = 0;
+    private Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<>();
+    private long mModificationTimestamp;
     private String mEtag = "";
     private final AtomicBoolean mCancellationRequested = new AtomicBoolean(false);
     
@@ -121,7 +121,7 @@ public class DownloadFileOperation extends RemoteOperation {
     }
 
     public String getMimeType() {
-        String mimeType = mFile.getMimetype();
+        String mimeType = mFile.getMimeType();
         if (mimeType == null || mimeType.length() <= 0) {
             try {
                 mimeType = MimeTypeMap.getSingleton()

+ 83 - 0
src/main/java/com/owncloud/android/operations/EmptyTrashbinFileOperation.java

@@ -0,0 +1,83 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2018 Tobias Kaminsky
+ * Copyright (C) 2018 Nextcloud GmbH.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU 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 <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.operations;
+
+import android.util.Log;
+
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.operations.common.SyncOperation;
+
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.jackrabbit.webdav.client.methods.DeleteMethod;
+
+import java.io.IOException;
+
+
+/**
+ * Empty trashbin.
+ */
+public class EmptyTrashbinFileOperation extends SyncOperation {
+
+    private static final String TAG = EmptyTrashbinFileOperation.class.getSimpleName();
+    private static final int RESTORE_READ_TIMEOUT = 30000;
+    private static final int RESTORE_CONNECTION_TIMEOUT = 5000;
+
+    private String userId;
+
+    /**
+     * Constructor
+     *
+     * @param userId to access correct trashbin
+     */
+    public EmptyTrashbinFileOperation(String userId) {
+        this.userId = userId;
+    }
+
+    /**
+     * Performs the operation.
+     *
+     * @param client Client object to communicate with the remote Nextcloud server.
+     */
+    @Override
+    protected RemoteOperationResult run(OwnCloudClient client) {
+
+        RemoteOperationResult result;
+        try {
+            DeleteMethod delete = new DeleteMethod(client.getNewWebdavUri() + "/trashbin/" + userId + "/trash");
+            int status = client.executeMethod(delete, RESTORE_READ_TIMEOUT, RESTORE_CONNECTION_TIMEOUT);
+
+            result = new RemoteOperationResult(isSuccess(status), delete);
+
+            client.exhaustResponse(delete.getResponseBodyAsStream());
+        } catch (IOException e) {
+            result = new RemoteOperationResult(e);
+            Log.e(TAG, "Empty trashbin failed: " + result.getLogMessage(), e);
+        }
+
+        return result;
+    }
+
+    private boolean isSuccess(int status) {
+        return status == HttpStatus.SC_NO_CONTENT;
+    }
+}

+ 105 - 97
src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java

@@ -22,6 +22,8 @@ package com.owncloud.android.operations;
 import android.accounts.Account;
 import android.content.Context;
 import android.content.Intent;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.util.Log;
 
 import com.owncloud.android.datamodel.DecryptedFolderMetadata;
@@ -104,9 +106,6 @@ public class RefreshFolderOperation extends RemoteOperation {
      */
     private boolean mSyncFullAccount;
 
-    /** 'True' means that Share resources bound to the files into should be refreshed also */
-    private boolean mIsShareSupported;
-
     /** 'True' means that the remote folder changed and should be fetched */
     private boolean mRemoteFolderChanged;
 
@@ -124,7 +123,6 @@ public class RefreshFolderOperation extends RemoteOperation {
      * @param   currentSyncTime         Time stamp for the synchronization process in progress.
      * @param   syncFullAccount         'True' means that this operation is part of a full account 
      *                                  synchronization.
-     * @param   isShareSupported        'True' means that the server supports the sharing API.           
      * @param   ignoreETag              'True' means that the content of the remote folder should
      *                                  be fetched and updated even though the 'eTag' did not 
      *                                  change.  
@@ -135,7 +133,6 @@ public class RefreshFolderOperation extends RemoteOperation {
     public RefreshFolderOperation(OCFile folder,
                                   long currentSyncTime,
                                   boolean syncFullAccount,
-                                  boolean isShareSupported,
                                   boolean ignoreETag,
                                   FileDataStorageManager dataStorageManager,
                                   Account account,
@@ -143,17 +140,15 @@ public class RefreshFolderOperation extends RemoteOperation {
         mLocalFolder = folder;
         mCurrentSyncTime = currentSyncTime;
         mSyncFullAccount = syncFullAccount;
-        mIsShareSupported = isShareSupported;
         mStorageManager = dataStorageManager;
         mAccount = account;
         mContext = context;
-        mForgottenLocalFiles = new HashMap<String, String>();
+        mForgottenLocalFiles = new HashMap<>();
         mRemoteFolderChanged = false;
         mIgnoreETag = ignoreETag;
-        mFilesToSyncContents = new Vector<SynchronizeFileOperation>();
+        mFilesToSyncContents = new Vector<>();
     }
 
-
     public int getConflictsFound() {
         return mConflictsFound;
     }
@@ -183,7 +178,7 @@ public class RefreshFolderOperation extends RemoteOperation {
      */
     @Override
     protected RemoteOperationResult run(OwnCloudClient client) {
-        RemoteOperationResult result = null;
+        RemoteOperationResult result;
         mFailsInKeptInSyncFound = 0;
         mConflictsFound = 0;
         mForgottenLocalFiles.clear();
@@ -218,7 +213,7 @@ public class RefreshFolderOperation extends RemoteOperation {
             );
         }
 
-        if (result.isSuccess() && mIsShareSupported && !mSyncFullAccount) {
+        if (result.isSuccess() && !mSyncFullAccount) {
             refreshSharesForFolder(client); // share result is ignored 
         }
 
@@ -236,14 +231,8 @@ public class RefreshFolderOperation extends RemoteOperation {
         UpdateOCVersionOperation update = new UpdateOCVersionOperation(mAccount, mContext);
         RemoteOperationResult result = update.execute(client);
         if (result.isSuccess()) {
-            mIsShareSupported = update.getOCVersion().isSharedSupported();
-
             // Update Capabilities for this account
-            if (update.getOCVersion().isVersionWithCapabilitiesAPI()) {
-                updateCapabilities();
-            } else {
-                Log_OC.d(TAG, "Capabilities API disabled");
-            }
+            updateCapabilities();
         }
     }
 
@@ -267,7 +256,7 @@ public class RefreshFolderOperation extends RemoteOperation {
 
     private RemoteOperationResult checkForChanges(OwnCloudClient client) {
         mRemoteFolderChanged = true;
-        RemoteOperationResult result = null;
+        RemoteOperationResult result;
         String remotePath = mLocalFolder.getRemotePath();
 
         Log_OC.d(TAG, "Checking changes in " + mAccount.name + remotePath);
@@ -341,9 +330,7 @@ public class RefreshFolderOperation extends RemoteOperation {
             mStorageManager.removeFolder(
                     mLocalFolder,
                     true,
-                    (mLocalFolder.isDown() &&
-                            mLocalFolder.getStoragePath().startsWith(currentSavePath)
-                    )
+                    mLocalFolder.isDown() && mLocalFolder.getStoragePath().startsWith(currentSavePath)
             );
         }
     }
@@ -368,32 +355,18 @@ public class RefreshFolderOperation extends RemoteOperation {
 
         Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath() + " changed - starting update of local data ");
 
-        List<OCFile> updatedFiles = new Vector<OCFile>(folderAndFiles.size() - 1);
+        List<OCFile> updatedFiles = new ArrayList<>(folderAndFiles.size() - 1);
         mFilesToSyncContents.clear();
 
         // if local folder is encrypted, download fresh metadata
-        DecryptedFolderMetadata metadata;
         boolean encryptedAncestor = FileStorageUtils.checkEncryptionStatus(mLocalFolder, mStorageManager);
         mLocalFolder.setEncrypted(encryptedAncestor);
-        
-        if (encryptedAncestor && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
-            metadata = EncryptionUtils.downloadFolderMetadata(mLocalFolder, getClient(), mContext, mAccount);
-        } else {
-            metadata = null;
-        }
 
-        // get current data about local contents of the folder to synchronize
-        List<OCFile> localFiles = mStorageManager.getFolderContent(mLocalFolder, false);
-        Map<String, OCFile> localFilesMap = new HashMap<String, OCFile>(localFiles.size());
+        DecryptedFolderMetadata metadata = getDecryptedFolderMetadata(encryptedAncestor);
 
-        for (OCFile file : localFiles) {
-            String remotePath = file.getRemotePath();
-
-            if (metadata != null && !file.isFolder()) {
-                remotePath = file.getParentRemotePath() + file.getEncryptedFileName();
-            }
-            localFilesMap.put(remotePath, file);
-        }
+        // get current data about local contents of the folder to synchronize
+        Map<String, OCFile> localFilesMap = prefillLocalFilesMap(metadata,
+                mStorageManager.getFolderContent(mLocalFolder, false));
 
         // loop to update every child
         OCFile remoteFile;
@@ -416,36 +389,8 @@ public class RefreshFolderOperation extends RemoteOperation {
             // add to updatedFile data about LOCAL STATE (not existing in server)
             updatedFile.setLastSyncDateForProperties(mCurrentSyncTime);
 
-            if (localFile != null) {
-                updatedFile.setFileId(localFile.getFileId());
-                updatedFile.setAvailableOffline(localFile.isAvailableOffline());
-                updatedFile.setLastSyncDateForData(localFile.getLastSyncDateForData());
-                updatedFile.setModificationTimestampAtLastSyncForData(
-                        localFile.getModificationTimestampAtLastSyncForData()
-                );
-                updatedFile.setStoragePath(localFile.getStoragePath());
-                // eTag will not be updated unless file CONTENTS are synchronized
-                if (!updatedFile.isFolder() && localFile.isDown() &&
-                        !updatedFile.getEtag().equals(localFile.getEtag())) {
-                    updatedFile.setEtagInConflict(updatedFile.getEtag());
-                }
-                updatedFile.setEtag(localFile.getEtag());
-                if (updatedFile.isFolder()) {
-                    updatedFile.setFileLength(remoteFile.getFileLength());
-                    updatedFile.setMountType(remoteFile.getMountType());
-                } else if (mRemoteFolderChanged && MimeTypeUtil.isImage(remoteFile) &&
-                        remoteFile.getModificationTimestamp() !=
-                                localFile.getModificationTimestamp()) {
-                    updatedFile.setNeedsUpdateThumbnail(true);
-                    Log.d(TAG, "Image " + remoteFile.getFileName() + " updated on the server");
-                }
-                updatedFile.setPublicLink(localFile.getPublicLink());
-                updatedFile.setShareViaLink(localFile.isSharedViaLink());
-                updatedFile.setShareWithSharee(localFile.isSharedWithSharee());
-            } else {
-                // remote eTag will not be updated unless file CONTENTS are synchronized
-                updatedFile.setEtag("");
-            }
+            // add to updatedFile data from local and remote file
+            setLocalFileDataOnUpdatedFile(remoteFile, localFile, updatedFile, mRemoteFolderChanged);
 
             // check and fix, if needed, local storage path
             FileStorageUtils.searchForLocalFileInDefaultPath(updatedFile, mAccount);
@@ -460,21 +405,7 @@ public class RefreshFolderOperation extends RemoteOperation {
 
             // update file name for encrypted files
             if (metadata != null) {
-                updatedFile.setEncryptedFileName(updatedFile.getFileName());
-                try {
-                    String decryptedFileName = metadata.getFiles().get(updatedFile.getFileName()).getEncrypted()
-                            .getFilename();
-                    String mimetype = metadata.getFiles().get(updatedFile.getFileName()).getEncrypted().getMimetype();
-                    updatedFile.setFileName(decryptedFileName);
-
-                    if (mimetype == null || mimetype.isEmpty()) {
-                        updatedFile.setMimetype("application/octet-stream");
-                    } else {
-                        updatedFile.setMimetype(mimetype);
-                    }
-                } catch (NullPointerException e) {
-                    Log_OC.e(TAG, "Metadata for file " + updatedFile.getFileId() + " not found!");
-                }
+                updateFileNameForEncryptedFile(metadata, updatedFile);
             }
 
             // we parse content, so either the folder itself or its direct parent (which we check) must be encrypted
@@ -490,6 +421,87 @@ public class RefreshFolderOperation extends RemoteOperation {
         mChildren = updatedFiles;
     }
 
+    @Nullable
+    private DecryptedFolderMetadata getDecryptedFolderMetadata(boolean encryptedAncestor) {
+        DecryptedFolderMetadata metadata;
+        if (encryptedAncestor && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
+            metadata = EncryptionUtils.downloadFolderMetadata(mLocalFolder, getClient(), mContext, mAccount);
+        } else {
+            metadata = null;
+        }
+        return metadata;
+    }
+
+    private void updateFileNameForEncryptedFile(@NonNull DecryptedFolderMetadata metadata, OCFile updatedFile) {
+        updatedFile.setEncryptedFileName(updatedFile.getFileName());
+        try {
+            String decryptedFileName = metadata.getFiles().get(updatedFile.getFileName()).getEncrypted()
+                    .getFilename();
+            String mimetype = metadata.getFiles().get(updatedFile.getFileName()).getEncrypted().getMimetype();
+            updatedFile.setFileName(decryptedFileName);
+
+            if (mimetype == null || mimetype.isEmpty()) {
+                updatedFile.setMimetype("application/octet-stream");
+            } else {
+                updatedFile.setMimetype(mimetype);
+            }
+        } catch (NullPointerException e) {
+            Log_OC.e(TAG, "Metadata for file " + updatedFile.getFileId() + " not found!");
+        }
+    }
+
+    private void setLocalFileDataOnUpdatedFile(OCFile remoteFile, OCFile localFile, OCFile updatedFile, boolean remoteFolderChanged) {
+        if (localFile != null) {
+            updatedFile.setFileId(localFile.getFileId());
+            updatedFile.setAvailableOffline(localFile.isAvailableOffline());
+            updatedFile.setLastSyncDateForData(localFile.getLastSyncDateForData());
+            updatedFile.setModificationTimestampAtLastSyncForData(
+                    localFile.getModificationTimestampAtLastSyncForData()
+            );
+            updatedFile.setStoragePath(localFile.getStoragePath());
+
+            // eTag will not be updated unless file CONTENTS are synchronized
+            if (!updatedFile.isFolder() && localFile.isDown() &&
+                    !updatedFile.getEtag().equals(localFile.getEtag())) {
+                updatedFile.setEtagInConflict(updatedFile.getEtag());
+            }
+
+            updatedFile.setEtag(localFile.getEtag());
+
+            if (updatedFile.isFolder()) {
+                updatedFile.setFileLength(remoteFile.getFileLength());
+                updatedFile.setMountType(remoteFile.getMountType());
+            } else if (remoteFolderChanged && MimeTypeUtil.isImage(remoteFile) &&
+                    remoteFile.getModificationTimestamp() !=
+                            localFile.getModificationTimestamp()) {
+                updatedFile.setNeedsUpdateThumbnail(true);
+                Log.d(TAG, "Image " + remoteFile.getFileName() + " updated on the server");
+            }
+
+            updatedFile.setPublicLink(localFile.getPublicLink());
+            updatedFile.setShareViaLink(localFile.isSharedViaLink());
+            updatedFile.setShareWithSharee(localFile.isSharedWithSharee());
+        } else {
+            // remote eTag will not be updated unless file CONTENTS are synchronized
+            updatedFile.setEtag("");
+        }
+    }
+
+    @NonNull
+    private Map<String, OCFile> prefillLocalFilesMap(DecryptedFolderMetadata metadata, List<OCFile> localFiles) {
+        Map<String, OCFile> localFilesMap = new HashMap<>(localFiles.size());
+
+        for (OCFile file : localFiles) {
+            String remotePath = file.getRemotePath();
+
+            if (metadata != null && !file.isFolder()) {
+                remotePath = file.getParentRemotePath() + file.getEncryptedFileName();
+            }
+            localFilesMap.put(remotePath, file);
+        }
+        return localFilesMap;
+    }
+
     /**
      * Performs a list of synchronization operations, determining if a download or upload is needed
      * or if exists conflict due to changes both in local and remote contents of the each file.
@@ -520,7 +532,6 @@ public class RefreshFolderOperation extends RemoteOperation {
         }
     }
 
-
     /**
      * Syncs the Share resources for the files contained in the folder refreshed (children, not deeper descendants).
      *
@@ -538,7 +549,7 @@ public class RefreshFolderOperation extends RemoteOperation {
 
         if (result.isSuccess()) {
             // update local database
-            ArrayList<OCShare> shares = new ArrayList<OCShare>();
+            ArrayList<OCShare> shares = new ArrayList<>();
             for (Object obj : result.getData()) {
                 shares.add((OCShare) obj);
             }
@@ -548,19 +559,16 @@ public class RefreshFolderOperation extends RemoteOperation {
         return result;
     }
 
-
     /**
-     * Sends a message to any application component interested in the progress 
+     * Sends a message to any application component interested in the progress
      * of the synchronization.
      *
-     * @param event
-     * @param dirRemotePath     Remote path of a folder that was just synchronized 
-     *                          (with or without success)
-     * @param result
+     * @param event         broadcast event (Intent Action)
+     * @param dirRemotePath Remote path of a folder that was just synchronized
+     *                      (with or without success)
+     * @param result        remote operation result
      */
-    private void sendLocalBroadcast(
-            String event, String dirRemotePath, RemoteOperationResult result
-    ) {
+    private void sendLocalBroadcast(String event, String dirRemotePath, RemoteOperationResult result) {
         Log_OC.d(TAG, "Send broadcast " + event);
         Intent intent = new Intent(event);
         intent.putExtra(FileSyncAdapter.EXTRA_ACCOUNT_NAME, mAccount.name);

+ 29 - 24
src/main/java/com/owncloud/android/operations/RemoveFileOperation.java

@@ -39,14 +39,13 @@ import com.owncloud.android.operations.common.SyncOperation;
  * Remote operation performing the removal of a remote file or folder in the ownCloud server.
  */
 public class RemoveFileOperation extends SyncOperation {
-    
-    // private static final String TAG = RemoveFileOperation.class.getSimpleName();
 
-    private OCFile mFileToRemove;
-    private String mRemotePath;
-    private boolean mOnlyLocalCopy;
-    private Account mAccount;
-    private Context mContext;
+    private OCFile fileToRemove;
+    private String remotePath;
+    private boolean onlyLocalCopy;
+    private Account account;
+    private boolean inBackground;
+    private Context context;
     
     
     /**
@@ -57,11 +56,13 @@ public class RemoveFileOperation extends SyncOperation {
      * @param onlyLocalCopy         When 'true', and a local copy of the file exists, only this is 
      *                              removed.
      */
-    public RemoveFileOperation(String remotePath, boolean onlyLocalCopy, Account account, Context context) {
-        mRemotePath = remotePath;
-        mOnlyLocalCopy = onlyLocalCopy;
-        mAccount = account;
-        mContext = context;
+    public RemoveFileOperation(String remotePath, boolean onlyLocalCopy, Account account, boolean inBackground,
+                               Context context) {
+        this.remotePath = remotePath;
+        this.onlyLocalCopy = onlyLocalCopy;
+        this.account = account;
+        this.inBackground = inBackground;
+        this.context = context;
     }
     
     
@@ -71,7 +72,11 @@ public class RemoveFileOperation extends SyncOperation {
      * @return      File to remove or already removed.
      */
     public OCFile getFile() {
-        return mFileToRemove;
+        return fileToRemove;
+    }
+
+    public boolean isInBackground() {
+        return inBackground;
     }
     
     /**
@@ -83,30 +88,30 @@ public class RemoveFileOperation extends SyncOperation {
     protected RemoteOperationResult run(OwnCloudClient client) {
         RemoteOperationResult result = null;
         RemoteOperation operation;
-        
-        mFileToRemove = getStorageManager().getFileByPath(mRemotePath);
+
+        fileToRemove = getStorageManager().getFileByPath(remotePath);
 
         // store resized image
-        ThumbnailsCacheManager.generateResizedImage(mFileToRemove);
+        ThumbnailsCacheManager.generateResizedImage(fileToRemove);
 
         boolean localRemovalFailed = false;
-        if (!mOnlyLocalCopy) {
+        if (!onlyLocalCopy) {
 
-            if (mFileToRemove.isEncrypted() &&
+            if (fileToRemove.isEncrypted() &&
                     android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
-                OCFile parent = getStorageManager().getFileByPath(mFileToRemove.getParentRemotePath());
-                operation = new RemoveRemoteEncryptedFileOperation(mRemotePath, parent.getLocalId(), mAccount, mContext,
-                        mFileToRemove.getEncryptedFileName());
+                OCFile parent = getStorageManager().getFileByPath(fileToRemove.getParentRemotePath());
+                operation = new RemoveRemoteEncryptedFileOperation(remotePath, parent.getLocalId(), account, context,
+                        fileToRemove.getEncryptedFileName());
             } else {
-                operation = new RemoveRemoteFileOperation(mRemotePath);
+                operation = new RemoveRemoteFileOperation(remotePath);
             }
             result = operation.execute(client);
             if (result.isSuccess() || result.getCode() == ResultCode.FILE_NOT_FOUND) {
-                localRemovalFailed = !(getStorageManager().removeFile(mFileToRemove, true, true));
+                localRemovalFailed = !(getStorageManager().removeFile(fileToRemove, true, true));
             }
             
         } else {
-            localRemovalFailed = !(getStorageManager().removeFile(mFileToRemove, false, true));
+            localRemovalFailed = !(getStorageManager().removeFile(fileToRemove, false, true));
             if (!localRemovalFailed) {
                 result = new RemoteOperationResult(ResultCode.OK);
             }

+ 1 - 1
src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.java

@@ -126,7 +126,7 @@ public class RemoveRemoteEncryptedFileOperation extends RemoteOperation {
             int status = client.executeMethod(delete, REMOVE_READ_TIMEOUT, REMOVE_CONNECTION_TIMEOUT);
 
             delete.getResponseBodyAsString();   // exhaust the response, although not interesting
-            result = new RemoteOperationResult((delete.succeeded() || status == HttpStatus.SC_NOT_FOUND), delete);
+            result = new RemoteOperationResult(delete.succeeded() || status == HttpStatus.SC_NOT_FOUND, delete);
             Log_OC.i(TAG, "Remove " + remotePath + ": " + result.getLogMessage());
 
             // remove file from metadata

+ 2 - 5
src/main/java/com/owncloud/android/operations/RenameFileOperation.java

@@ -46,8 +46,6 @@ public class RenameFileOperation extends SyncOperation {
     private String mRemotePath;
     private String mNewName;
     private String mNewRemotePath;
-
-    
     
     /**
      * Constructor
@@ -114,7 +112,7 @@ public class RenameFileOperation extends SyncOperation {
         } catch (IOException e) {
             Log_OC.e(TAG, "Rename " + mFile.getRemotePath() + " to " + ((mNewRemotePath==null) ?
                     mNewName : mNewRemotePath) + ": " +
-                    ((result!= null) ? result.getLogMessage() : ""), e);
+                    (result!= null ? result.getLogMessage() : ""), e);
         }
 
         return result;
@@ -184,7 +182,7 @@ public class RenameFileOperation extends SyncOperation {
             Log_OC.i(TAG, "Test for validity of name " + mNewName + " in the file system failed");
             return false;
         }
-        boolean result = (testFile.exists() && testFile.isFile());
+        boolean result = testFile.exists() && testFile.isFile();
         
         // cleaning ; result is ignored, since there is not much we could do in case of failure,
         // but repeat and repeat...
@@ -192,5 +190,4 @@ public class RenameFileOperation extends SyncOperation {
         
         return result;
     }
-
 }

+ 92 - 0
src/main/java/com/owncloud/android/operations/RestoreFileVersionOperation.java

@@ -0,0 +1,92 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2018 Tobias Kaminsky
+ * Copyright (C) 2018 Nextcloud GmbH.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU 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 <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.operations;
+
+import android.util.Log;
+
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.operations.common.SyncOperation;
+
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.jackrabbit.webdav.client.methods.MoveMethod;
+
+import java.io.IOException;
+
+
+/**
+ * Restore a {@link com.owncloud.android.lib.resources.files.FileVersion}.
+ */
+public class RestoreFileVersionOperation extends SyncOperation {
+
+    private static final String TAG = RestoreFileVersionOperation.class.getSimpleName();
+    private static final int RESTORE_READ_TIMEOUT = 30000;
+    private static final int RESTORE_CONNECTION_TIMEOUT = 5000;
+
+    private String fileId;
+    private String fileName;
+    private String userId;
+
+    /**
+     * Constructor
+     *
+     * @param fileId fileId
+     * @param fileName version date in unixtime
+     * @param userId userId to access correct dav endpoint
+     */
+    public RestoreFileVersionOperation(String fileId, String fileName, String userId) {
+        this.fileId = fileId;
+        this.fileName = fileName;
+        this.userId = userId;
+    }
+
+    /**
+     * Performs the operation.
+     *
+     * @param client Client object to communicate with the remote ownCloud server.
+     */
+    @Override
+    protected RemoteOperationResult run(OwnCloudClient client) {
+
+        RemoteOperationResult result;
+        try {
+            String source = client.getNewWebdavUri() + "/versions/" + userId + "/versions/" + fileId + "/" + fileName;
+            String target = client.getNewWebdavUri() + "/versions/" + userId + "/restore/" + fileId;
+
+            MoveMethod move = new MoveMethod(source, target, true);
+            int status = client.executeMethod(move, RESTORE_READ_TIMEOUT, RESTORE_CONNECTION_TIMEOUT);
+
+            result = new RemoteOperationResult(isSuccess(status), move);
+
+            client.exhaustResponse(move.getResponseBodyAsStream());
+        } catch (IOException e) {
+            result = new RemoteOperationResult(e);
+            Log.e(TAG, "Restore file version with id " + fileId + " failed: " + result.getLogMessage(), e);
+        }
+
+        return result;
+    }
+
+    private boolean isSuccess(int status) {
+        return status == HttpStatus.SC_CREATED || status == HttpStatus.SC_NO_CONTENT;
+    }
+}

+ 93 - 0
src/main/java/com/owncloud/android/operations/RestoreTrashbinFileOperation.java

@@ -0,0 +1,93 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2018 Tobias Kaminsky
+ * Copyright (C) 2018 Nextcloud GmbH.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU 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 <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.operations;
+
+import android.util.Log;
+
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.network.WebdavUtils;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.operations.common.SyncOperation;
+
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.jackrabbit.webdav.client.methods.MoveMethod;
+
+import java.io.IOException;
+
+
+/**
+ * Restore a {@link com.owncloud.android.lib.resources.files.TrashbinFile}.
+ */
+public class RestoreTrashbinFileOperation extends SyncOperation {
+
+    private static final String TAG = RestoreTrashbinFileOperation.class.getSimpleName();
+    private static final int RESTORE_READ_TIMEOUT = 30000;
+    private static final int RESTORE_CONNECTION_TIMEOUT = 5000;
+
+    private String sourcePath;
+    private String fileName;
+    private String userId;
+
+    /**
+     * Constructor
+     *
+     * @param sourcePath Remote path of the {@link com.owncloud.android.lib.resources.files.TrashbinFile} to restore
+     * @param fileName   original filename
+     * @param userId     userId to access correct trashbin
+     */
+    public RestoreTrashbinFileOperation(String sourcePath, String fileName, String userId) {
+        this.sourcePath = sourcePath;
+        this.fileName = fileName;
+        this.userId = userId;
+    }
+
+    /**
+     * Performs the operation.
+     *
+     * @param client Client object to communicate with the remote ownCloud server.
+     */
+    @Override
+    protected RemoteOperationResult run(OwnCloudClient client) {
+
+        RemoteOperationResult result;
+        try {
+            String source = client.getNewWebdavUri() + WebdavUtils.encodePath(sourcePath);
+            String target = client.getNewWebdavUri() + "/trashbin/" + userId + "/restore/" + fileName;
+
+            MoveMethod move = new MoveMethod(source, target, true);
+            int status = client.executeMethod(move, RESTORE_READ_TIMEOUT, RESTORE_CONNECTION_TIMEOUT);
+
+            result = new RemoteOperationResult(isSuccess(status), move);
+
+            client.exhaustResponse(move.getResponseBodyAsStream());
+        } catch (IOException e) {
+            result = new RemoteOperationResult(e);
+            Log.e(TAG, "Restore trashbin file " + sourcePath + " failed: " + result.getLogMessage(), e);
+        }
+
+        return result;
+    }
+
+    private boolean isSuccess(int status) {
+        return status == HttpStatus.SC_CREATED || status == HttpStatus.SC_NO_CONTENT;
+    }
+}

+ 1 - 1
src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java

@@ -210,7 +210,7 @@ public class SynchronizeFileOperation extends SyncOperation {
                     serverChanged = mServerFile.getModificationTimestamp() !=
                             mLocalFile.getModificationTimestampAtLastSyncForData();
                 } else {
-                    serverChanged = (!mServerFile.getEtag().equals(mLocalFile.getEtag()));
+                    serverChanged = !mServerFile.getEtag().equals(mLocalFile.getEtag());
                 }
                 boolean localChanged = (
                         mLocalFile.getLocalModificationTimestamp() > mLocalFile.getLastSyncDateForData()

+ 1 - 2
src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java

@@ -42,7 +42,6 @@ import com.owncloud.android.utils.FileStorageUtils;
 import com.owncloud.android.utils.MimeTypeUtil;
 
 import java.io.File;
-import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -261,7 +260,7 @@ public class SynchronizeFolderOperation extends SyncOperation {
      *
      * @param folderAndFiles Remote folder and children files in Folder
      */
-    private void synchronizeData(ArrayList<Object> folderAndFiles) throws OperationCancelledException {
+    private void synchronizeData(List<Object> folderAndFiles) throws OperationCancelledException {
         FileDataStorageManager storageManager = getStorageManager();
         
         // parse data from remote folder

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

@@ -34,7 +34,7 @@ import com.owncloud.android.lib.resources.shares.RemoveRemoteShareOperation;
 import com.owncloud.android.lib.resources.shares.ShareType;
 import com.owncloud.android.operations.common.SyncOperation;
 
-import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Unshare file/folder
@@ -80,7 +80,7 @@ public class UnshareOperation extends SyncOperation {
                 } else if (ShareType.USER.equals(mShareType) || ShareType.GROUP.equals(mShareType)
                     || ShareType.FEDERATED.equals(mShareType)){
                     // Check if it is the last share
-                    ArrayList <OCShare> sharesWith = getStorageManager().
+                    List <OCShare> sharesWith = getStorageManager().
                             getSharesWithForAFile(mRemotePath,
                             getStorageManager().getAccount().name);
                     if (sharesWith.size() == 1) {

+ 21 - 27
src/main/java/com/owncloud/android/operations/UploadFileOperation.java

@@ -25,6 +25,7 @@ import android.content.Context;
 import android.net.Uri;
 import android.os.Build;
 import android.support.annotation.RequiresApi;
+import android.util.Log;
 
 import com.evernote.android.job.JobRequest;
 import com.evernote.android.job.util.Device;
@@ -111,19 +112,18 @@ public class UploadFileOperation extends SyncOperation {
      * (if forceOverwrite==false and remote file already exists).
      */
     private OCFile mOldFile;
-    private String mRemotePath = null;
+    private String mRemotePath;
     private String mFolderUnlockToken;
-    private boolean mChunked = false;
-    private boolean mRemoteFolderToBeCreated = false;
-    private boolean mForceOverwrite = false;
+    private boolean mRemoteFolderToBeCreated;
+    private boolean mForceOverwrite;
     private int mLocalBehaviour = FileUploader.LOCAL_BEHAVIOUR_COPY;
     private int mCreatedBy = CREATED_BY_USER;
-    private boolean mOnWifiOnly = false;
-    private boolean mWhileChargingOnly = false;
-    private boolean mIgnoringPowerSaveMode = false;
+    private boolean mOnWifiOnly;
+    private boolean mWhileChargingOnly;
+    private boolean mIgnoringPowerSaveMode;
 
-    private boolean mWasRenamed = false;
-    private long mOCUploadId = -1;
+    private boolean mWasRenamed;
+    private long mOCUploadId;
     /**
      * Local path to file which is to be uploaded (before any possible renaming or moving).
      */
@@ -175,7 +175,6 @@ public class UploadFileOperation extends SyncOperation {
     public UploadFileOperation(Account account,
                                OCFile file,
                                OCUpload upload,
-                               boolean chunked,
                                boolean forceOverwrite,
                                int localBehaviour,
                                Context context,
@@ -208,13 +207,12 @@ public class UploadFileOperation extends SyncOperation {
         mOnWifiOnly = onWifiOnly;
         mWhileChargingOnly = whileChargingOnly;
         mRemotePath = upload.getRemotePath();
-        mChunked = chunked;
         mForceOverwrite = forceOverwrite;
         mLocalBehaviour = localBehaviour;
         mOriginalStoragePath = mFile.getStoragePath();
         mContext = context;
         mOCUploadId = upload.getUploadId();
-        mCreatedBy = upload.getCreadtedBy();
+        mCreatedBy = upload.getCreatedBy();
         mRemoteFolderToBeCreated = upload.isCreateRemoteFolder();
         // Ignore power save mode only if user explicitly created this upload
         mIgnoringPowerSaveMode = (mCreatedBy == CREATED_BY_USER);
@@ -268,7 +266,7 @@ public class UploadFileOperation extends SyncOperation {
     }
 
     public String getMimeType() {
-        return mFile.getMimetype();
+        return mFile.getMimeType();
     }
 
     public int getLocalBehaviour() {
@@ -342,10 +340,6 @@ public class UploadFileOperation extends SyncOperation {
         mRenameUploadListener = listener;
     }
 
-    public boolean isChunkedUploadSupported() {
-        return mChunked;
-    }
-
     public Context getContext() {
         return mContext;
     }
@@ -579,13 +573,13 @@ public class UploadFileOperation extends SyncOperation {
             }
 
             /// perform the upload
-            if (mChunked && (size > ChunkedUploadRemoteFileOperation.CHUNK_SIZE)) {
+            if (size > ChunkedUploadRemoteFileOperation.CHUNK_SIZE) {
                 mUploadOperation = new ChunkedUploadRemoteFileOperation(mContext, encryptedTempFile.getAbsolutePath(),
-                        mFile.getParentRemotePath() + encryptedFileName, mFile.getMimetype(),
+                        mFile.getParentRemotePath() + encryptedFileName, mFile.getMimeType(),
                         mFile.getEtagInConflict(), timeStamp);
             } else {
                 mUploadOperation = new UploadRemoteFileOperation(encryptedTempFile.getAbsolutePath(),
-                        mFile.getParentRemotePath() + encryptedFileName, mFile.getMimetype(),
+                        mFile.getParentRemotePath() + encryptedFileName, mFile.getMimeType(),
                         mFile.getEtagInConflict(), timeStamp);
             }
 
@@ -611,7 +605,7 @@ public class UploadFileOperation extends SyncOperation {
                 DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
                 DecryptedFolderMetadata.Data data = new DecryptedFolderMetadata.Data();
                 data.setFilename(mFile.getFileName());
-                data.setMimetype(mFile.getMimetype());
+                data.setMimetype(mFile.getMimeType());
                 data.setKey(EncryptionUtils.encodeBytesToBase64String(key));
 
                 decryptedFile.setEncrypted(data);
@@ -820,12 +814,12 @@ public class UploadFileOperation extends SyncOperation {
             }
 
             // perform the upload
-            if (mChunked && (size > ChunkedUploadRemoteFileOperation.CHUNK_SIZE)) {
+            if (size > ChunkedUploadRemoteFileOperation.CHUNK_SIZE) {
                 mUploadOperation = new ChunkedUploadRemoteFileOperation(mContext, mFile.getStoragePath(),
-                        mFile.getRemotePath(), mFile.getMimetype(), mFile.getEtagInConflict(), timeStamp);
+                        mFile.getRemotePath(), mFile.getMimeType(), mFile.getEtagInConflict(), timeStamp);
             } else {
                 mUploadOperation = new UploadRemoteFileOperation(mFile.getStoragePath(),
-                        mFile.getRemotePath(), mFile.getMimetype(), mFile.getEtagInConflict(), timeStamp);
+                        mFile.getRemotePath(), mFile.getMimeType(), mFile.getEtagInConflict(), timeStamp);
             }
 
             Iterator<OnDatatransferProgressListener> listener = mDataTransferListeners.iterator();
@@ -983,7 +977,7 @@ public class UploadFileOperation extends SyncOperation {
                 try {
                     move(originalFile, newFile);
                 } catch (IOException e) {
-                    e.printStackTrace();
+                    Log.e(TAG, "Error moving file", e);
                 }
                 getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath());
                 mFile.setStoragePath(newFile.getAbsolutePath());
@@ -1055,7 +1049,7 @@ public class UploadFileOperation extends SyncOperation {
         OCFile newFile = new OCFile(newRemotePath);
         newFile.setCreationTimestamp(mFile.getCreationTimestamp());
         newFile.setFileLength(mFile.getFileLength());
-        newFile.setMimetype(mFile.getMimetype());
+        newFile.setMimetype(mFile.getMimeType());
         newFile.setModificationTimestamp(mFile.getModificationTimestamp());
         newFile.setModificationTimestampAtLastSyncForData(
                 mFile.getModificationTimestampAtLastSyncForData()
@@ -1173,7 +1167,7 @@ public class UploadFileOperation extends SyncOperation {
 
         RemoteOperationResult result = null;
 
-        if (FileStorageUtils.getUsableSpace(mAccount.name) < sourceFile.length()) {
+        if (FileStorageUtils.getUsableSpace() < sourceFile.length()) {
             result = new RemoteOperationResult(ResultCode.LOCAL_STORAGE_FULL);
             return result;  // error condition when the file should be copied
 

+ 1 - 1
src/main/java/com/owncloud/android/providers/DiskLruImageCacheFileProvider.java

@@ -105,7 +105,7 @@ public class DiskLruImageCacheFileProvider extends ContentProvider {
     @Override
     public String getType(@NonNull Uri uri) {
         OCFile ocFile = getFile(uri);
-        return ocFile.getMimetype();
+        return ocFile.getMimeType();
     }
 
     @Override

+ 5 - 4
src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java

@@ -57,16 +57,17 @@ import org.nextcloud.providers.cursors.RootCursor;
 
 import java.io.File;
 import java.io.FileNotFoundException;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
-import java.util.Vector;
 
 @TargetApi(Build.VERSION_CODES.KITKAT)
 public class DocumentsStorageProvider extends DocumentsProvider {
 
     private static final String TAG = "DocumentsStorageProvider";
 
-    private FileDataStorageManager mCurrentStorageManager = null;
+    private FileDataStorageManager mCurrentStorageManager;
     private static Map<Long, FileDataStorageManager> mRootIdToStorageManager;
 
     @Override
@@ -291,8 +292,8 @@ public class DocumentsStorageProvider extends DocumentsProvider {
         return !(cancellationSignal != null && cancellationSignal.isCanceled());
     }
 
-    Vector<OCFile> findFiles(OCFile root, String query) {
-        Vector<OCFile> result = new Vector<>();
+    List<OCFile> findFiles(OCFile root, String query) {
+        List<OCFile> result = new ArrayList<>();
         for (OCFile f : mCurrentStorageManager.getFolderContent(root, false)) {
             if (f.isFolder()) {
                 result.addAll(findFiles(f, query));

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

@@ -52,6 +52,7 @@ import com.owncloud.android.lib.common.operations.OnRemoteOperationListener;
 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.lib.resources.files.FileVersion;
 import com.owncloud.android.lib.resources.shares.ShareType;
 import com.owncloud.android.lib.resources.status.OwnCloudVersion;
 import com.owncloud.android.lib.resources.users.GetRemoteUserInfoOperation;
@@ -65,6 +66,7 @@ import com.owncloud.android.operations.MoveFileOperation;
 import com.owncloud.android.operations.OAuth2GetAccessToken;
 import com.owncloud.android.operations.RemoveFileOperation;
 import com.owncloud.android.operations.RenameFileOperation;
+import com.owncloud.android.operations.RestoreFileVersionOperation;
 import com.owncloud.android.operations.SynchronizeFileOperation;
 import com.owncloud.android.operations.SynchronizeFolderOperation;
 import com.owncloud.android.operations.UnshareOperation;
@@ -93,6 +95,7 @@ public class OperationsService extends Service {
     public static final String EXTRA_RESULT = "RESULT";
     public static final String EXTRA_NEW_PARENT_PATH = "NEW_PARENT_PATH";
     public static final String EXTRA_FILE = "FILE";
+    public static final String EXTRA_FILE_VERSION = "FILE_VERSION";
     public static final String EXTRA_SHARE_PASSWORD = "SHARE_PASSWORD";
     public static final String EXTRA_SHARE_TYPE = "SHARE_TYPE";
     public static final String EXTRA_SHARE_WITH = "SHARE_WITH";
@@ -100,6 +103,8 @@ public class OperationsService extends Service {
     public static final String EXTRA_SHARE_PERMISSIONS = "SHARE_PERMISSIONS";
     public static final String EXTRA_SHARE_PUBLIC_UPLOAD = "SHARE_PUBLIC_UPLOAD";
     public static final String EXTRA_SHARE_ID = "SHARE_ID";
+    public static final String EXTRA_USER_ID = "USER_ID";
+    public static final String EXTRA_IN_BACKGROUND = "IN_BACKGROUND";
 
     public static final String EXTRA_COOKIE = "COOKIE";
 
@@ -119,6 +124,7 @@ public class OperationsService extends Service {
     public static final String ACTION_MOVE_FILE = "MOVE_FILE";
     public static final String ACTION_COPY_FILE = "COPY_FILE";
     public static final String ACTION_CHECK_CURRENT_CREDENTIALS = "CHECK_CURRENT_CREDENTIALS";
+    public static final String ACTION_RESTORE_VERSION = "RESTORE_VERSION";
 
     public static final String ACTION_OPERATION_ADDED = OperationsService.class.getName() + ".OPERATION_ADDED";
     public static final String ACTION_OPERATION_FINISHED = OperationsService.class.getName() + ".OPERATION_FINISHED";
@@ -132,9 +138,9 @@ public class OperationsService extends Service {
             mUndispatchedFinishedOperations = new ConcurrentHashMap<>();
 
     private static class Target {
-        public Uri mServerUrl = null;
-        public Account mAccount = null;
-        public String mCookie = null;
+        public Uri mServerUrl;
+        public Account mAccount;
+        public String mCookie;
 
         public Target(Account account, Uri serverUrl, String cookie) {
             mAccount = account;
@@ -176,7 +182,7 @@ public class OperationsService extends Service {
 
         // WIP: for the moment, only SYNC_FOLDER is expected here;
         // the rest of the operations are requested through the Binder
-        if (ACTION_SYNC_FOLDER.equals(intent.getAction())) {
+        if (intent != null && ACTION_SYNC_FOLDER.equals(intent.getAction())) {
 
             if (!intent.hasExtra(EXTRA_ACCOUNT) || !intent.hasExtra(EXTRA_REMOTE_PATH)) {
                 Log_OC.e(TAG, "Not enough information provided in intent");
@@ -325,7 +331,7 @@ public class OperationsService extends Service {
          *          in process.
          */
         public boolean isPerformingBlockingOperation() {
-            return (!mServiceHandler.mPendingOperations.isEmpty());
+            return !mServiceHandler.mPendingOperations.isEmpty();
         }
 
 
@@ -516,9 +522,6 @@ public class OperationsService extends Service {
                 mService.dispatchResultToOperationListeners(mCurrentOperation, result);
             }
         }
-
-
-
     }
 
 
@@ -551,7 +554,7 @@ public class OperationsService extends Service {
                 
                 String action = operationIntent.getAction();
 
-                if (action.equals(ACTION_CREATE_SHARE_VIA_LINK)) {  // Create public share via link
+                if (ACTION_CREATE_SHARE_VIA_LINK.equals(action)) {  // Create public share via link
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
                     String password = operationIntent.getStringExtra(EXTRA_SHARE_PASSWORD);
                     if (remotePath.length() > 0) {
@@ -594,7 +597,7 @@ public class OperationsService extends Service {
                         ((UpdateSharePermissionsOperation)operation).setPassword(password);
                     }
 
-                } else if (action.equals(ACTION_CREATE_SHARE_WITH_SHAREE)) {
+                } else if (ACTION_CREATE_SHARE_WITH_SHAREE.equals(action)) {
                     // Create private share with user or group
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
                     String shareeName = operationIntent.getStringExtra(EXTRA_SHARE_WITH);
@@ -609,7 +612,7 @@ public class OperationsService extends Service {
                         );
                     }
 
-                } else if (action.equals(ACTION_UNSHARE)) {  // Unshare file
+                } else if (ACTION_UNSHARE.equals(action)) {  // Unshare file
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
                     ShareType shareType = (ShareType) operationIntent.
                             getSerializableExtra(EXTRA_SHARE_TYPE);
@@ -623,11 +626,11 @@ public class OperationsService extends Service {
                         );
                     }
                     
-                } else if (action.equals(ACTION_GET_SERVER_INFO)) { 
+                } else if (ACTION_GET_SERVER_INFO.equals(action)) {
                     // check OC server and get basic information from it
                     operation = new GetServerInfoOperation(serverUrl, OperationsService.this);
 
-                } else if (action.equals(ACTION_OAUTH2_GET_ACCESS_TOKEN)) {
+                } else if (ACTION_OAUTH2_GET_ACCESS_TOKEN.equals(action)) {
                     /// GET ACCESS TOKEN to the OAuth server
                     String oauth2QueryParameters =
                             operationIntent.getStringExtra(EXTRA_OAUTH2_QUERY_PARAMETERS);
@@ -637,36 +640,38 @@ public class OperationsService extends Service {
                             getString(R.string.oauth2_grant_type),
                             oauth2QueryParameters);
 
-                } else if (action.equals(ACTION_GET_USER_NAME)) {
+                } else if (ACTION_GET_USER_NAME.equals(action)) {
                     // Get User Name
                     operation = new GetRemoteUserInfoOperation();
                     
-                } else if (action.equals(ACTION_RENAME)) {
+                } else if (ACTION_RENAME.equals(action)) {
                     // Rename file or folder
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
                     String newName = operationIntent.getStringExtra(EXTRA_NEWNAME);
                     operation = new RenameFileOperation(remotePath, newName);
                     
-                } else if (action.equals(ACTION_REMOVE)) {
+                } else if (ACTION_REMOVE.equals(action)) {
                     // Remove file or folder
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
                     boolean onlyLocalCopy = operationIntent.getBooleanExtra(EXTRA_REMOVE_ONLY_LOCAL, false);
-                    operation = new RemoveFileOperation(remotePath, onlyLocalCopy, account, getApplicationContext());
+                    boolean inBackground = operationIntent.getBooleanExtra(EXTRA_IN_BACKGROUND, false);
+                    operation = new RemoveFileOperation(remotePath, onlyLocalCopy, account, inBackground,
+                            getApplicationContext());
                     
-                } else if (action.equals(ACTION_CREATE_FOLDER)) {
+                } else if (ACTION_CREATE_FOLDER.equals(action)) {
                     // Create Folder
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
                     boolean createFullPath = operationIntent.getBooleanExtra(EXTRA_CREATE_FULL_PATH, true);
                     operation = new CreateFolderOperation(remotePath, createFullPath);
 
-                } else if (action.equals(ACTION_SYNC_FILE)) {
+                } else if (ACTION_SYNC_FILE.equals(action)) {
                     // Sync file
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
                     boolean syncFileContents = operationIntent.getBooleanExtra(EXTRA_SYNC_FILE_CONTENTS, true);
                     operation = new SynchronizeFileOperation(remotePath, account, syncFileContents,
                             getApplicationContext());
                     
-                } else if (action.equals(ACTION_SYNC_FOLDER)) {
+                } else if (ACTION_SYNC_FOLDER.equals(action)) {
                     // Sync folder (all its descendant files are sync'ed)
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
                     operation = new SynchronizeFolderOperation(
@@ -676,21 +681,26 @@ public class OperationsService extends Service {
                             System.currentTimeMillis()  // TODO remove this dependency from construction time
                     );
 
-                } else if (action.equals(ACTION_MOVE_FILE)) {
+                } else if (ACTION_MOVE_FILE.equals(action)) {
                     // Move file/folder
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
                     String newParentPath = operationIntent.getStringExtra(EXTRA_NEW_PARENT_PATH);
                     operation = new MoveFileOperation(remotePath, newParentPath);
 
-                } else if (action.equals(ACTION_COPY_FILE)) {
+                } else if (ACTION_COPY_FILE.equals(action)) {
                     // Copy file/folder
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
                     String newParentPath = operationIntent.getStringExtra(EXTRA_NEW_PARENT_PATH);
                     operation = new CopyFileOperation(remotePath, newParentPath);
 
-                } else if (action.equals(ACTION_CHECK_CURRENT_CREDENTIALS)) {
+                } else if (ACTION_CHECK_CURRENT_CREDENTIALS.equals(action)) {
                     // Check validity of currently stored credentials for a given account
                     operation = new CheckCurrentCredentialsOperation(account);
+                } else if (ACTION_RESTORE_VERSION.equals(action)) {
+                    FileVersion fileVersion = operationIntent.getParcelableExtra(EXTRA_FILE_VERSION);
+                    String userId = operationIntent.getStringExtra(EXTRA_USER_ID);
+                    operation = new RestoreFileVersionOperation(fileVersion.getRemoteId(), fileVersion.getFileName(),
+                            userId);
                 }
             }
                 

+ 2 - 5
src/main/java/com/owncloud/android/services/SyncFolderHandler.java

@@ -51,12 +51,11 @@ class SyncFolderHandler extends Handler {
 
     private static final String TAG = SyncFolderHandler.class.getSimpleName();
 
-
     private OperationsService mService;
 
     private IndexedForest<SynchronizeFolderOperation> mPendingOperations = new IndexedForest<>();
 
-    private Account mCurrentAccount = null;
+    private Account mCurrentAccount;
     private FileDataStorageManager mStorageManager;
     private SynchronizeFolderOperation mCurrentSyncOperation;
 
@@ -69,7 +68,6 @@ class SyncFolderHandler extends Handler {
         mService = service;
     }
 
-
     /**
      * Returns True when the folder located in 'remotePath' in the ownCloud account 'account', or any of its
      * descendants, is being synchronized (or waiting for it).
@@ -81,10 +79,9 @@ class SyncFolderHandler extends Handler {
         if (account == null || remotePath == null) {
             return false;
         }
-        return (mPendingOperations.contains(account.name, remotePath));
+        return mPendingOperations.contains(account.name, remotePath);
     }
 
-
     @Override
     public void handleMessage(Message msg) {
         Pair<Account, String> itemSyncKey = (Pair<Account, String>) msg.obj;

+ 1 - 1
src/main/java/com/owncloud/android/syncadapter/AbstractOwnCloudSyncAdapter.java

@@ -52,7 +52,7 @@ public abstract class AbstractOwnCloudSyncAdapter extends
     private ContentProviderClient mContentProviderClient;
     private FileDataStorageManager mStoreManager;
 
-    private OwnCloudClient mClient = null;
+    private OwnCloudClient mClient;
 
     public AbstractOwnCloudSyncAdapter(Context context, boolean autoInitialize) {
         super(context, autoInitialize);

+ 1 - 8
src/main/java/com/owncloud/android/syncadapter/FileSyncAdapter.java

@@ -112,10 +112,6 @@ public class FileSyncAdapter extends AbstractOwnCloudSyncAdapter {
     /** {@link SyncResult} instance to return to the system when the synchronization finish */
     private SyncResult mSyncResult;
 
-    /** 'True' means that the server supports the share API */
-    private boolean mIsShareSupported;
-    
-    
     /**
      * Creates a {@link FileSyncAdapter}
      *
@@ -238,8 +234,6 @@ public class FileSyncAdapter extends AbstractOwnCloudSyncAdapter {
         RemoteOperationResult result = update.execute(getClient());
         if (!result.isSuccess()) {
             mLastFailedResult = result; 
-        } else {
-            mIsShareSupported = update.getOCVersion().isSharedSupported();
         }
     }
     
@@ -265,7 +259,6 @@ public class FileSyncAdapter extends AbstractOwnCloudSyncAdapter {
         RefreshFolderOperation synchFolderOp = new RefreshFolderOperation( folder,
                                                                                    mCurrentSyncTime,
                                                                                    true,
-                                                                                   mIsShareSupported,
                                                                                    false,
                                                                                    getStorageManager(),
                                                                                    getAccount(),
@@ -532,7 +525,7 @@ public class FileSyncAdapter extends AbstractOwnCloudSyncAdapter {
         NotificationManager notificationManager = ((NotificationManager) getContext().
                 getSystemService(Context.NOTIFICATION_SERVICE));
 
-        if ((android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O)) {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
             builder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_FILE_SYNC);
         }
 

+ 1 - 1
src/main/java/com/owncloud/android/syncadapter/FileSyncService.java

@@ -33,7 +33,7 @@ import android.os.IBinder;
 public class FileSyncService extends Service {
     
     // Storage for an instance of the sync adapter
-    private static FileSyncAdapter sSyncAdapter = null;
+    private static FileSyncAdapter sSyncAdapter;
     // Object to use as a thread-safe lock
     private static final Object sSyncAdapterLock = new Object();
     

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

@@ -39,7 +39,7 @@ public class CustomPopup {
     protected final View mAnchor;
     protected final PopupWindow mWindow;
     private View root;
-    private Drawable background = null;
+    private Drawable background;
     protected final WindowManager mWManager;
 
     public CustomPopup(View anchor) {
@@ -57,8 +57,7 @@ public class CustomPopup {
             }
         });
 
-        mWManager = (WindowManager) anchor.getContext().getSystemService(
-                Context.WINDOW_SERVICE);
+        mWManager = (WindowManager) anchor.getContext().getSystemService(Context.WINDOW_SERVICE);
         onCreate();
     }
 

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

@@ -31,10 +31,9 @@ import android.view.View;
  * Extends RecyclerView to show a custom view if no data is available
  * Inspired by http://alexzh.com/tutorials/how-to-setemptyview-to-recyclerview
  */
-
 public class EmptyRecyclerView extends RecyclerView {
     private View mEmptyView;
-    private boolean hasFooter = false;
+    private boolean hasFooter;
 
     public EmptyRecyclerView(Context context) {
         super(context);

+ 4 - 3
src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java

@@ -170,7 +170,7 @@ public class ActivitiesActivity extends FileActivity implements ActivityListInte
                 PorterDuff.Mode.SRC_IN);
 
         FileDataStorageManager storageManager = new FileDataStorageManager(getAccount(), getContentResolver());
-        adapter = new ActivityListAdapter(this, this, storageManager);
+        adapter = new ActivityListAdapter(this, this, storageManager, false);
         recyclerView.setAdapter(adapter);
 
         LinearLayoutManager layoutManager = new LinearLayoutManager(this);
@@ -229,14 +229,15 @@ public class ActivitiesActivity extends FileActivity implements ActivityListInte
     protected void onResume() {
         super.onResume();
 
+        setDrawerMenuItemChecked(R.id.nav_activity);
+        
         setupContent();
     }
 
     @Override
     public void onActivityClicked(RichObject richObject) {
         String path = FileUtils.PATH_SEPARATOR + richObject.getPath();
-        mActionListener.openActivity(path, this,
-                getFileOperationsHelper().isSharedSupported());
+        mActionListener.openActivity(path, this);
     }
 
     @Override

+ 2 - 1
src/main/java/com/owncloud/android/ui/activities/ActivitiesContract.java

@@ -39,7 +39,8 @@ public interface ActivitiesContract {
 
     interface ActionListener {
         void loadActivities(String pageUrl);
-        void openActivity(String fileUrl, BaseActivity baseActivity, boolean isSharingSupported);
+
+        void openActivity(String fileUrl, BaseActivity baseActivity);
 
         void stopLoadingActivity();
     }

+ 3 - 4
src/main/java/com/owncloud/android/ui/activities/ActivitiesPresenter.java

@@ -35,7 +35,7 @@ public class ActivitiesPresenter implements ActivitiesContract.ActionListener {
     private final ActivitiesContract.View activitiesView;
     private final ActivitiesRepository activitiesRepository;
     private final FilesRepository filesRepository;
-    private boolean activityStopped = false;
+    private boolean activityStopped;
 
 
     ActivitiesPresenter(@NonNull ActivitiesRepository activitiesRepository,
@@ -71,10 +71,9 @@ public class ActivitiesPresenter implements ActivitiesContract.ActionListener {
     }
 
     @Override
-    public void openActivity(String fileUrl, BaseActivity baseActivity, boolean isSharingSupported) {
+    public void openActivity(String fileUrl, BaseActivity baseActivity) {
         activitiesView.setProgressIndicatorState(true);
-        filesRepository.readRemoteFile(fileUrl, baseActivity, isSharingSupported,
-                new FilesRepository.ReadRemoteFileCallback() {
+        filesRepository.readRemoteFile(fileUrl, baseActivity, new FilesRepository.ReadRemoteFileCallback() {
                     @Override
                     public void onFileLoaded(@Nullable OCFile ocFile) {
                         activitiesView.setProgressIndicatorState(false);

+ 1 - 2
src/main/java/com/owncloud/android/ui/activities/data/files/FilesRepository.java

@@ -33,6 +33,5 @@ public interface FilesRepository {
         void onFileLoadError(String error);
     }
 
-    void readRemoteFile(String path, BaseActivity activity, boolean isSharingSupported,
-                        @NonNull ReadRemoteFileCallback callback);
+    void readRemoteFile(String path, BaseActivity activity, @NonNull ReadRemoteFileCallback callback);
 }

+ 1 - 2
src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApi.java

@@ -32,6 +32,5 @@ public interface FilesServiceApi {
         void onError(String error);
     }
 
-    void readRemoteFile(String fileUrl, BaseActivity activity, boolean isSharingSupported,
-                        FilesServiceApi.FilesServiceCallback<OCFile> callback);
+    void readRemoteFile(String fileUrl, BaseActivity activity, FilesServiceApi.FilesServiceCallback<OCFile> callback);
 }

+ 3 - 10
src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApiImpl.java

@@ -49,10 +49,8 @@ public class FilesServiceApiImpl implements FilesServiceApi {
     private static final String TAG = FilesServiceApiImpl.class.getSimpleName();
 
     @Override
-    public void readRemoteFile(String fileUrl, BaseActivity activity,
-                               boolean isSharingSupported, FilesServiceCallback<OCFile> callback) {
-        ReadRemoteFileTask readRemoteFileTask = new ReadRemoteFileTask(fileUrl, activity,
-                isSharingSupported, callback);
+    public void readRemoteFile(String fileUrl, BaseActivity activity, FilesServiceCallback<OCFile> callback) {
+        ReadRemoteFileTask readRemoteFileTask = new ReadRemoteFileTask(fileUrl, activity, callback);
         readRemoteFileTask.execute();
     }
 
@@ -62,15 +60,11 @@ public class FilesServiceApiImpl implements FilesServiceApi {
         private String errorMessage;
         // TODO: Figure out a better way to do this than passing a BaseActivity reference.
         private final BaseActivity baseActivity;
-        private final boolean isSharingSupported;
         private final String fileUrl;
 
-        private ReadRemoteFileTask(String fileUrl, BaseActivity baseActivity,
-                                   boolean isSharingSupported,
-                                   FilesServiceCallback<OCFile> callback) {
+        private ReadRemoteFileTask(String fileUrl, BaseActivity baseActivity, FilesServiceCallback<OCFile> callback) {
             this.callback = callback;
             this.baseActivity = baseActivity;
-            this.isSharingSupported = isSharingSupported;
             this.fileUrl = fileUrl;
         }
 
@@ -97,7 +91,6 @@ public class FilesServiceApiImpl implements FilesServiceApi {
                         RemoteOperation synchFolderOp = new RefreshFolderOperation(remoteOcFile,
                                 System.currentTimeMillis(),
                                 false,
-                                isSharingSupported,
                                 true,
                                 baseActivity.getStorageManager(),
                                 baseActivity.getAccount(),

+ 2 - 3
src/main/java/com/owncloud/android/ui/activities/data/files/RemoteFilesRepository.java

@@ -33,9 +33,8 @@ class RemoteFilesRepository implements FilesRepository {
 
 
     @Override
-    public void readRemoteFile(String path, BaseActivity activity, boolean isSharingSupported, @NonNull ReadRemoteFileCallback callback) {
-        filesServiceApi.readRemoteFile(path, activity, isSharingSupported,
-                new FilesServiceApi.FilesServiceCallback<OCFile>() {
+    public void readRemoteFile(String path, BaseActivity activity, @NonNull ReadRemoteFileCallback callback) {
+        filesServiceApi.readRemoteFile(path, activity, new FilesServiceApi.FilesServiceCallback<OCFile>() {
                     @Override
                     public void onLoaded(OCFile ocFile) {
                         callback.onFileLoaded(ocFile);

+ 6 - 7
src/main/java/com/owncloud/android/ui/activity/BaseActivity.java

@@ -36,7 +36,7 @@ public abstract class BaseActivity extends AppCompatActivity {
     /**
      * Flag to signal that the activity will is finishing to enforce the creation of an ownCloud {@link Account}.
      */
-    private boolean mRedirectingToSetupAccount = false;
+    private boolean mRedirectingToSetupAccount;
 
     /**
      * Flag to signal when the value of mAccount was set.
@@ -51,7 +51,7 @@ public abstract class BaseActivity extends AppCompatActivity {
     /**
      * Access point to the cached database for the current ownCloud {@link Account}.
      */
-    private FileDataStorageManager mStorageManager = null;
+    private FileDataStorageManager mStorageManager;
 
     @Override
     protected void onNewIntent (Intent intent) {
@@ -71,7 +71,7 @@ public abstract class BaseActivity extends AppCompatActivity {
     protected void onRestart() {
         Log_OC.v(TAG, "onRestart() start");
         super.onRestart();
-        boolean validAccount = (mCurrentAccount != null && AccountUtils.exists(mCurrentAccount, this));
+        boolean validAccount = mCurrentAccount != null && AccountUtils.exists(mCurrentAccount, this);
         if (!validAccount) {
             swapToDefaultAccount();
         }
@@ -91,12 +91,11 @@ public abstract class BaseActivity extends AppCompatActivity {
     protected void setAccount(Account account, boolean savedAccount) {
         Account oldAccount = mCurrentAccount;
         boolean validAccount =
-                (account != null && AccountUtils.setCurrentOwnCloudAccount(getApplicationContext(),
-                        account.name));
+                account != null && AccountUtils.setCurrentOwnCloudAccount(getApplicationContext(), account.name);
         if (validAccount) {
             mCurrentAccount = account;
             mAccountWasSet = true;
-            mAccountWasRestored = (savedAccount || mCurrentAccount.equals(oldAccount));
+            mAccountWasRestored = savedAccount || mCurrentAccount.equals(oldAccount);
 
         } else {
             swapToDefaultAccount();
@@ -123,7 +122,7 @@ public abstract class BaseActivity extends AppCompatActivity {
 
         } else {
             mAccountWasSet = true;
-            mAccountWasRestored = (newAccount.equals(mCurrentAccount));
+            mAccountWasRestored = newAccount.equals(mCurrentAccount);
             mCurrentAccount = newAccount;
         }
     }

+ 6 - 42
src/main/java/com/owncloud/android/ui/activity/CopyToClipboardActivity.java

@@ -1,8 +1,10 @@
-/**
+/*
  *   ownCloud Android client application
  *
  *   @author David A. Velasco
+ *   @author Andy Scherzinger
  *   Copyright (C) 2015 ownCloud Inc.
+ *   Copyright (C) 2018 Andy Scherzinger
  *
  *   This program is free software: you can redistribute it and/or modify
  *   it under the terms of the GNU General Public License version 2,
@@ -15,65 +17,27 @@
  *
  *   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.activity;
 
 import android.app.Activity;
-import android.content.ClipData;
-import android.content.ClipboardManager;
 import android.content.Intent;
 import android.os.Bundle;
 
-import com.owncloud.android.R;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.ClipboardUtil;
 
 /**
- * Activity copying the text of the received Intent into the system clibpoard.
+ * Activity copying the text of the received Intent into the system clipboard.
  */
-@SuppressWarnings("deprecation")
 public class CopyToClipboardActivity extends Activity {
 
-    private static final String TAG = CopyToClipboardActivity.class.getName();
-
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        try {
-
-            // get the clipboard system service
-            ClipboardManager clipboardManager = (ClipboardManager) this.getSystemService(CLIPBOARD_SERVICE);
-
-            // get the text to copy into the clipboard
-            Intent intent = getIntent();
-            CharSequence text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT);
-
-            if (text != null && text.length() > 0) {
-                // minimum API level >= 11 -> only modern Clipboard
-                ClipData clip = ClipData.newPlainText(
-                    getString(R.string.clipboard_label, getString(R.string.app_name)),
-                    text
-                );
-                clipboardManager.setPrimaryClip(clip);
-
-                // API level < 11 -> legacy Clipboard - NOT SUPPORTED ANYMORE
-                // clipboardManager.setText(text);
-
-                // alert the user that the text is in the clipboard and we're done
-                DisplayUtils.showSnackMessage(this, R.string.clipboard_text_copied);
-            } else {
-                DisplayUtils.showSnackMessage(this, R.string.clipboard_no_text_to_copy);
-            }
-
-        } catch (Exception e) {
-            DisplayUtils.showSnackMessage(this, R.string.clipboard_uxexpected_error);
-            Log_OC.e(TAG, "Exception caught while copying to clipboard", e);
-        }
+        ClipboardUtil.copyToClipboard(this, getIntent().getCharSequenceExtra(Intent.EXTRA_TEXT).toString());
 
         finish();
     }
-
 }

+ 35 - 63
src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java

@@ -37,6 +37,7 @@ import android.graphics.drawable.LayerDrawable;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
+import android.support.annotation.NonNull;
 import android.support.design.widget.NavigationView;
 import android.support.v4.view.GravityCompat;
 import android.support.v4.widget.DrawerLayout;
@@ -86,7 +87,9 @@ import com.owncloud.android.ui.events.ChangeMenuEvent;
 import com.owncloud.android.ui.events.DummyDrawerEvent;
 import com.owncloud.android.ui.events.MenuItemClickEvent;
 import com.owncloud.android.ui.events.SearchEvent;
+import com.owncloud.android.ui.trashbin.TrashbinActivity;
 import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.DrawerMenuUtil;
 import com.owncloud.android.utils.FilesSyncHelper;
 import com.owncloud.android.utils.ThemeUtils;
 import com.owncloud.android.utils.svg.MenuSimpleTarget;
@@ -96,6 +99,7 @@ import org.greenrobot.eventbus.Subscribe;
 import org.greenrobot.eventbus.ThreadMode;
 
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Base class to handle setup of the drawer implementation including user switching and avatar fetching and fallback
@@ -223,7 +227,9 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
 
         setupDrawerToggle();
 
-        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+        if(getSupportActionBar() != null) {
+            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+        }
     }
 
     /**
@@ -313,7 +319,7 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
         navigationView.setNavigationItemSelectedListener(
                 new NavigationView.OnNavigationItemSelectedListener() {
                     @Override
-                    public boolean onNavigationItemSelected(final MenuItem menuItem) {
+                    public boolean onNavigationItemSelected(@NonNull final MenuItem menuItem) {
                         mDrawerLayout.closeDrawers();
                         // pending runnable will be executed after the drawer has been closed
                         pendingRunnable = new Runnable() {
@@ -334,66 +340,25 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
         }
 
         Account account = AccountUtils.getCurrentOwnCloudAccount(this);
-        boolean searchSupported = AccountUtils.hasSearchSupport(account);
-
-        if (getResources().getBoolean(R.bool.bottom_toolbar_enabled) && account != null) {
-            navigationView.getMenu().removeItem(R.id.nav_all_files);
-            navigationView.getMenu().removeItem(R.id.nav_settings);
-            navigationView.getMenu().removeItem(R.id.nav_favorites);
-            navigationView.getMenu().removeItem(R.id.nav_photos);
-        }
-
-        if (!searchSupported && account != null) {
-            navigationView.getMenu().removeItem(R.id.nav_photos);
-            navigationView.getMenu().removeItem(R.id.nav_favorites);
-            navigationView.getMenu().removeItem(R.id.nav_videos);
-        }
-
-        if (getResources().getBoolean(R.bool.use_home) && navigationView.getMenu().findItem(R.id.nav_all_files) !=
-                null) {
-            navigationView.getMenu().findItem(R.id.nav_all_files).setTitle(getResources().
-                    getString(R.string.drawer_item_home));
-            navigationView.getMenu().findItem(R.id.nav_all_files).setIcon(R.drawable.ic_home);
-        }
-
-        if (!getResources().getBoolean(R.bool.participate_enabled)) {
-            navigationView.getMenu().removeItem(R.id.nav_participate);
-        }
-
-        if (!getResources().getBoolean(R.bool.shared_enabled)) {
-            navigationView.getMenu().removeItem(R.id.nav_shared);
-        }
-
-        if (!getResources().getBoolean(R.bool.contacts_backup)
-                || !getResources().getBoolean(R.bool.show_drawer_contacts_backup)) {
-            navigationView.getMenu().removeItem(R.id.nav_contacts);
-        }
-
-        if (getResources().getBoolean(R.bool.syncedFolder_light)) {
-            navigationView.getMenu().removeItem(R.id.nav_synced_folders);
-        }
+        filterDrawerMenu(navigationView.getMenu(), account);
+    }
 
-        if (!getResources().getBoolean(R.bool.show_drawer_logout)) {
-            navigationView.getMenu().removeItem(R.id.nav_logout);
-        }
+    private void filterDrawerMenu(Menu menu, Account account) {
+        DrawerMenuUtil.filterForBottomToolbarMenuItems(menu, getResources());
+        DrawerMenuUtil.filterSearchMenuItems(menu, account, getResources());
+        DrawerMenuUtil.filterTrashbinMenuItems(menu, account, getContentResolver());
 
-        if (AccountUtils.hasSearchSupport(account)) {
-            if (!getResources().getBoolean(R.bool.recently_added_enabled)) {
-                navigationView.getMenu().removeItem(R.id.nav_recently_added);
-            }
+        DrawerMenuUtil.setupHomeMenuItem(menu, getResources());
 
-            if (!getResources().getBoolean(R.bool.recently_modified_enabled)) {
-                navigationView.getMenu().removeItem(R.id.nav_recently_modified);
-            }
+        DrawerMenuUtil.removeMenuItem(menu, R.id.nav_participate,
+                !getResources().getBoolean(R.bool.participate_enabled));
+        DrawerMenuUtil.removeMenuItem(menu, R.id.nav_shared, !getResources().getBoolean(R.bool.shared_enabled));
+        DrawerMenuUtil.removeMenuItem(menu, R.id.nav_contacts, !getResources().getBoolean(R.bool.contacts_backup)
+                || !getResources().getBoolean(R.bool.show_drawer_contacts_backup));
 
-            if (!getResources().getBoolean(R.bool.videos_enabled)) {
-                navigationView.getMenu().removeItem(R.id.nav_videos);
-            }
-        } else if (account != null) {
-            navigationView.getMenu().removeItem(R.id.nav_recently_added);
-            navigationView.getMenu().removeItem(R.id.nav_recently_modified);
-            navigationView.getMenu().removeItem(R.id.nav_videos);
-        }
+        DrawerMenuUtil.removeMenuItem(menu, R.id.nav_synced_folders,
+                getResources().getBoolean(R.bool.syncedFolder_light));
+        DrawerMenuUtil.removeMenuItem(menu, R.id.nav_logout, !getResources().getBoolean(R.bool.show_drawer_logout));
     }
 
     @Subscribe(threadMode = ThreadMode.MAIN)
@@ -445,6 +410,11 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
                 uploadListIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                 startActivity(uploadListIntent);
                 break;
+            case R.id.nav_trashbin:
+                Intent trashbinIntent = new Intent(getApplicationContext(), TrashbinActivity.class);
+                trashbinIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                startActivity(trashbinIntent);
+                break;
             case R.id.nav_activity:
                 Intent activityIntent = new Intent(getApplicationContext(), ActivitiesActivity.class);
                 activityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
@@ -643,7 +613,7 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
                     accountEndView.setTag(mAvatars[1].name);
 
                     DisplayUtils.setAvatar(mAvatars[1], this, mOtherAccountAvatarRadiusDimension, getResources(),
-                            getStorageManager(), accountEndView, this);
+                            accountEndView, this);
                     mAccountEndAccountAvatar.setVisibility(View.VISIBLE);
                 } else {
                     mAccountEndAccountAvatar.setVisibility(View.GONE);
@@ -655,7 +625,7 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
                     accountMiddleView.setTag(mAvatars[2].name);
 
                     DisplayUtils.setAvatar(mAvatars[2], this, mOtherAccountAvatarRadiusDimension, getResources(),
-                            getStorageManager(), accountMiddleView, this);
+                            accountMiddleView, this);
                     mAccountMiddleAccountAvatar.setVisibility(View.VISIBLE);
                 } else {
                     mAccountMiddleAccountAvatar.setVisibility(View.GONE);
@@ -689,7 +659,7 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
                             account.name)
                             .setIcon(TextDrawable.createAvatar(account.name, mMenuAccountAvatarRadiusDimension));
                     DisplayUtils.setAvatar(account, this, mMenuAccountAvatarRadiusDimension, getResources(),
-                            getStorageManager(), accountMenuItem, this);
+                            accountMenuItem, this);
                 }
             } catch (Exception e) {
                 Log_OC.e(TAG, "Error calculating RGB value for account menu item.", e);
@@ -761,7 +731,7 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
             currentAccountView.setTag(account.name);
 
             DisplayUtils.setAvatar(account, this, mCurrentAccountAvatarRadiusDimension, getResources(),
-                    getStorageManager(), currentAccountView, this);
+                    currentAccountView, this);
 
             // check and show quota info if available
             getAndDisplayUserQuota();
@@ -855,7 +825,7 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
     private void updateQuotaLink() {
         if (mQuotaTextLink != null) {
             if (getBaseContext().getResources().getBoolean(R.bool.show_external_links)) {
-                ArrayList<ExternalLink> quotas = externalLinksProvider.getExternalLink(ExternalLinkType.QUOTA);
+                List<ExternalLink> quotas = externalLinksProvider.getExternalLink(ExternalLinkType.QUOTA);
 
                 float density = getResources().getDisplayMetrics().density;
                 final int size = Math.round(24 * density);
@@ -1044,6 +1014,8 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
 
                 DisplayUtils.downloadIcon(this, link.iconUrl, target, R.drawable.ic_link_grey, size, size);
             }
+
+            setDrawerMenuItemChecked(mCheckedMenuItem);
         }
     }
 

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

@@ -48,7 +48,7 @@ import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.FileStorageUtils;
 
 import java.io.File;
-import java.util.ArrayList;
+import java.util.List;
 
 
 /**
@@ -75,8 +75,8 @@ public class ErrorsWhileCopyingHandlerActivity  extends AppCompatActivity implem
     
     protected Account mAccount;
     protected FileDataStorageManager mStorageManager;
-    protected ArrayList<String> mLocalPaths;
-    protected ArrayList<String> mRemotePaths;
+    protected List<String> mLocalPaths;
+    protected List<String> mRemotePaths;
     protected ArrayAdapter<String> mAdapter;
     protected Handler mHandler;
     private DialogFragment mCurrentDialog;

+ 8 - 4
src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java

@@ -53,7 +53,7 @@ public class ExternalSiteWebView extends FileActivity {
 
     private static final String TAG = ExternalSiteWebView.class.getSimpleName();
 
-    private boolean showSidebar = false;
+    private boolean showSidebar;
     private int menuItemId;
     private WebView webview;
 
@@ -97,7 +97,12 @@ public class ExternalSiteWebView extends FileActivity {
         ActionBar actionBar = getSupportActionBar();
         if (actionBar != null) {
             ThemeUtils.setColoredTitle(actionBar, title, this);
-            actionBar.setDisplayHomeAsUpEnabled(true);
+
+            if (showSidebar) {
+                actionBar.setDisplayHomeAsUpEnabled(true);
+            } else {
+                setDrawerIndicatorEnabled(false);
+            }
         }
 
         // enable zoom
@@ -157,8 +162,7 @@ public class ExternalSiteWebView extends FileActivity {
                         openDrawer();
                     }
                 } else {
-                    Intent settingsIntent = new Intent(getApplicationContext(), Preferences.class);
-                    startActivity(settingsIntent);
+                    finish();
                 }
             retval = true;
             break;

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

@@ -118,15 +118,16 @@ public abstract class FileActivity extends DrawerActivity
 
     private FileOperationsHelper mFileOperationsHelper;
 
-    private ServiceConnection mOperationsServiceConnection = null;
+    private ServiceConnection mOperationsServiceConnection;
 
-    private OperationsServiceBinder mOperationsServiceBinder = null;
+    private OperationsServiceBinder mOperationsServiceBinder;
 
-    private boolean mResumed = false;
+    private boolean mResumed;
 
-    protected FileDownloaderBinder mDownloaderBinder = null;
-    protected FileUploaderBinder mUploaderBinder = null;
-    private ServiceConnection mDownloadServiceConnection, mUploadServiceConnection = null;
+    protected FileDownloaderBinder mDownloaderBinder;
+    protected FileUploaderBinder mUploaderBinder;
+    private ServiceConnection mDownloadServiceConnection;
+    private ServiceConnection mUploadServiceConnection;
 
 
 
@@ -384,7 +385,7 @@ public abstract class FileActivity extends DrawerActivity
             }
             OwnCloudClient client;
             OwnCloudAccount ocAccount = new OwnCloudAccount(account, context);
-            client = (OwnCloudClientManagerFactory.getDefaultSingleton().removeClientFor(ocAccount));
+            client = OwnCloudClientManagerFactory.getDefaultSingleton().removeClientFor(ocAccount);
             if (client != null) {
                 OwnCloudCredentials cred = client.getCredentials();
                 if (cred != null) {

+ 183 - 79
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -69,6 +69,7 @@ import android.widget.ImageView;
 
 import com.owncloud.android.MainApp;
 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.datamodel.VirtualFolderType;
@@ -85,6 +86,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCo
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.shares.OCShare;
 import com.owncloud.android.lib.resources.shares.ShareType;
+import com.owncloud.android.lib.resources.status.OwnCloudVersion;
 import com.owncloud.android.media.MediaService;
 import com.owncloud.android.media.MediaServiceBinder;
 import com.owncloud.android.operations.CopyFileOperation;
@@ -95,6 +97,7 @@ import com.owncloud.android.operations.MoveFileOperation;
 import com.owncloud.android.operations.RefreshFolderOperation;
 import com.owncloud.android.operations.RemoveFileOperation;
 import com.owncloud.android.operations.RenameFileOperation;
+import com.owncloud.android.operations.RestoreFileVersionOperation;
 import com.owncloud.android.operations.SynchronizeFileOperation;
 import com.owncloud.android.operations.UnshareOperation;
 import com.owncloud.android.operations.UpdateSharePermissionsOperation;
@@ -137,6 +140,7 @@ import org.parceler.Parcels;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
 
 import static com.owncloud.android.db.PreferenceManager.getSortOrder;
 
@@ -154,7 +158,7 @@ public class FileDisplayActivity extends HookActivity
     private SyncBroadcastReceiver mSyncBroadcastReceiver;
     private UploadFinishReceiver mUploadFinishReceiver;
     private DownloadFinishReceiver mDownloadFinishReceiver;
-    private RemoteOperationResult mLastSslUntrustedServerResult = null;
+    private RemoteOperationResult mLastSslUntrustedServerResult;
 
     private boolean mDualPane;
     private View mLeftFragmentContainer;
@@ -188,14 +192,14 @@ public class FileDisplayActivity extends HookActivity
 
     private OCFile mWaitingToPreview;
 
-    private boolean mSyncInProgress = false;
+    private boolean mSyncInProgress;
 
     private OCFile mWaitingToSend;
 
     private Collection<MenuItem> mDrawerMenuItemstoShowHideList;
 
-    private MediaServiceBinder mMediaServiceBinder =  null;
-    private MediaServiceConnection mMediaServiceConnection = null;
+    private MediaServiceBinder mMediaServiceBinder;
+    private MediaServiceConnection mMediaServiceConnection;
 
     private String searchQuery;
 
@@ -236,7 +240,6 @@ public class FileDisplayActivity extends HookActivity
             setupDrawer(R.id.nav_all_files);
         }
 
-
         mDualPane = getResources().getBoolean(R.bool.large_land_layout);
         mLeftFragmentContainer = findViewById(R.id.left_fragment_container);
         mRightFragmentContainer = findViewById(R.id.right_fragment_container);
@@ -299,6 +302,7 @@ public class FileDisplayActivity extends HookActivity
         // always AFTER setContentView(...) in onCreate(); to work around bug in its implementation
 
         upgradeNotificationForInstantUpload();
+        checkOutdatedServer();
     }
 
     private Activity getActivity() {
@@ -351,6 +355,28 @@ public class FileDisplayActivity extends HookActivity
         }
     }
 
+    private void checkOutdatedServer() {
+        Account account = getAccount();
+
+        if (getResources().getBoolean(R.bool.show_outdated_server_warning) && account != null) {
+            ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(getContentResolver());
+
+            int lastSeenVersion = arbitraryDataProvider.getIntegerValue(account,
+                    WhatsNewActivity.KEY_LAST_SEEN_VERSION_CODE);
+
+            if (MainApp.getVersionCode() > lastSeenVersion) {
+                OwnCloudVersion serverVersion = AccountUtils.getServerVersionForAccount(account, this);
+
+                if (serverVersion.compareTo(MainApp.OUTDATED_SERVER_VERSION) < 0) {
+                    DisplayUtils.showServerOutdatedSnackbar(this);
+                }
+
+                arbitraryDataProvider.storeOrUpdateKeyValue(account.name, WhatsNewActivity.KEY_LAST_SEEN_VERSION_CODE,
+                        String.valueOf(MainApp.getVersionCode()));
+            }
+        }
+    }
+
     @Override
     public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
                                            @NonNull int[] grantResults) {
@@ -481,7 +507,7 @@ public class FileDisplayActivity extends HookActivity
                 updateActionBarTitleAndHomeButton(file);
             } else {
                 cleanSecondFragment();
-                if (file.isDown() && MimeTypeUtil.isVCard(file.getMimetype())) {
+                if (file.isDown() && MimeTypeUtil.isVCard(file.getMimeType())) {
                     startContactListFragment(file);
                 } else if (file.isDown() && PreviewTextFragment.canBePreviewed(file)) {
                     startTextPreview(file, false);
@@ -503,7 +529,7 @@ public class FileDisplayActivity extends HookActivity
     @Override
     protected void onNewIntent(Intent intent) {
         super.onNewIntent(intent);
-        if (intent.getAction() != null && intent.getAction().equalsIgnoreCase(ACTION_DETAILS)) {
+        if (ACTION_DETAILS.equalsIgnoreCase(intent.getAction())) {
             setIntent(intent);
             setFile(intent.getParcelableExtra(EXTRA_FILE));
         } else if (RESTART.equals(intent.getAction())) {
@@ -554,17 +580,11 @@ public class FileDisplayActivity extends HookActivity
             return OCShare.READ_PERMISSION_FLAG;    // minimum permissions
 
         } else if (isFederated) {
-            if (com.owncloud.android.authentication.AccountUtils
-                    .getServerVersion(getAccount()).isNotReshareableFederatedSupported()) {
-                return (getFile().isFolder() ? OCShare.FEDERATED_PERMISSIONS_FOR_FOLDER_AFTER_OC9 :
-                        OCShare.FEDERATED_PERMISSIONS_FOR_FILE_AFTER_OC9);
-            } else {
-                return (getFile().isFolder() ? OCShare.FEDERATED_PERMISSIONS_FOR_FOLDER_UP_TO_OC9 :
-                        OCShare.FEDERATED_PERMISSIONS_FOR_FILE_UP_TO_OC9);
-            }
+            return getFile().isFolder() ? OCShare.FEDERATED_PERMISSIONS_FOR_FOLDER_AFTER_OC9 :
+                    OCShare.FEDERATED_PERMISSIONS_FOR_FILE_AFTER_OC9;
         } else {
-            return (getFile().isFolder() ? OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER :
-                    OCShare.MAXIMUM_PERMISSIONS_FOR_FILE);
+            return getFile().isFolder() ? OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER :
+                    OCShare.MAXIMUM_PERMISSIONS_FOR_FILE;
         }
     }
 
@@ -588,7 +608,7 @@ public class FileDisplayActivity extends HookActivity
     /**
      * Replaces the second fragment managed by the activity with the received as
      * a parameter.
-     * <p/>
+     *
      * Assumes never will be more than two fragments managed at the same time.
      *
      * @param fragment New second Fragment to set.
@@ -705,13 +725,15 @@ public class FileDisplayActivity extends HookActivity
                 boolean detailsFragmentChanged = false;
                 if (waitedPreview) {
                     if (success) {
-                        mWaitingToPreview = getStorageManager().getFileById(
-                                mWaitingToPreview.getFileId());   // update the file from database,
-                        // for the local storage path
+                        // update the file from database, for the local storage path
+                        mWaitingToPreview = getStorageManager().getFileById(mWaitingToPreview.getFileId());
+                        
                         if (PreviewMediaFragment.canBePreviewed(mWaitingToPreview)) {
-                            startMediaPreview(mWaitingToPreview, 0, true, true);
+                            boolean streaming = AccountUtils.getServerVersionForAccount(getAccount(), this)
+                                    .isMediaStreamingSupported();
+                            startMediaPreview(mWaitingToPreview, 0, true, true, streaming);
                             detailsFragmentChanged = true;
-                        } else if (MimeTypeUtil.isVCard(mWaitingToPreview.getMimetype())) {
+                        } else if (MimeTypeUtil.isVCard(mWaitingToPreview.getMimeType())) {
                             startContactListFragment(mWaitingToPreview);
                             detailsFragmentChanged = true;
                         } else if (PreviewTextFragment.canBePreviewed(mWaitingToPreview)) {
@@ -789,6 +811,11 @@ public class FileDisplayActivity extends HookActivity
                     setDrawerIndicatorEnabled(isDrawerIndicatorAvailable()); // order matters
                     getSupportActionBar().setDisplayHomeAsUpEnabled(true);
                     mDrawerToggle.syncState();
+
+                    if (getListOfFilesFragment() != null) {
+                        getListOfFilesFragment().setSearchFragment(false);
+                        getListOfFilesFragment().refreshDirectory();
+                    }
                 } else {
                     searchView.post(new Runnable() {
                         @Override
@@ -974,9 +1001,7 @@ public class FileDisplayActivity extends HookActivity
                 remotePaths[j] = remotePathBase + (new File(filePaths[j])).getName();
             }
 
-            // default, as fallback
-            int behaviour = FileUploader.LOCAL_BEHAVIOUR_FORGET;
-
+            int behaviour;
             switch (resultCode) {
                 case UploadFilesActivity.RESULT_OK_AND_MOVE:
                     behaviour = FileUploader.LOCAL_BEHAVIOUR_MOVE;
@@ -989,6 +1014,10 @@ public class FileDisplayActivity extends HookActivity
                 case UploadFilesActivity.RESULT_OK_AND_DO_NOTHING:
                     behaviour = FileUploader.LOCAL_BEHAVIOUR_FORGET;
                     break;
+
+                default:
+                    behaviour = FileUploader.LOCAL_BEHAVIOUR_FORGET;
+                    break;
             }
 
             FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
@@ -1113,7 +1142,7 @@ public class FileDisplayActivity extends HookActivity
             // all closed
 
             //if PreviewImageActivity called this activity and mDualPane==false  then calls PreviewImageActivity again
-            if ((getIntent().getAction() != null && getIntent().getAction().equalsIgnoreCase(ACTION_DETAILS)) && !mDualPane) {
+            if (ACTION_DETAILS.equalsIgnoreCase(getIntent().getAction()) && !mDualPane) {
                 getIntent().setAction(null);
                 getIntent().putExtra(EXTRA_FILE, (OCFile) null);
                 startImagePreview(getFile(), false);
@@ -1166,11 +1195,11 @@ public class FileDisplayActivity extends HookActivity
         }
 
         revertBottomNavigationBarToAllFiles();
-        // refresh list of files
 
+        // refresh list of files
         if (searchView != null && !TextUtils.isEmpty(searchQuery)) {
             searchView.setQuery(searchQuery, true);
-        } else if (getListOfFilesFragment() != null && !getListOfFilesFragment().getIsSearchFragment()
+        } else if (getListOfFilesFragment() != null && !getListOfFilesFragment().isSearchFragment()
                 && startFile == null) {
             refreshListOfFilesFragment(false);
         } else {
@@ -1179,7 +1208,7 @@ public class FileDisplayActivity extends HookActivity
         }
 
         // Listen for sync messages
-        if (getListOfFilesFragment() != null && !getListOfFilesFragment().getIsSearchFragment()) {
+        if (getListOfFilesFragment() != null && !getListOfFilesFragment().isSearchFragment()) {
             IntentFilter syncIntentFilter = new IntentFilter(FileSyncAdapter.EVENT_FULL_SYNC_START);
             syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_END);
             syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED);
@@ -1203,8 +1232,14 @@ public class FileDisplayActivity extends HookActivity
         mDownloadFinishReceiver = new DownloadFinishReceiver();
         registerReceiver(mDownloadFinishReceiver, downloadIntentFilter);
 
+        // setup drawer
+        if (MainApp.isOnlyOnDevice()) {
+            setDrawerMenuItemChecked(R.id.nav_on_device);
+        } else {
+            setDrawerMenuItemChecked(R.id.nav_all_files);
+        }
+        
         Log_OC.v(TAG, "onResume() end");
-
     }
 
 
@@ -1296,25 +1331,28 @@ public class FileDisplayActivity extends HookActivity
                             setFile(currentFile);
                         }
 
-                        mSyncInProgress = (!FileSyncAdapter.EVENT_FULL_SYNC_END.equals(event) &&
-                                !RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.equals(event));
+                        mSyncInProgress = !FileSyncAdapter.EVENT_FULL_SYNC_END.equals(event) &&
+                                !RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.equals(event);
 
                         if (RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED.equals(event) &&
-                                synchResult != null && !synchResult.isSuccess()) {
-
-                            /// TODO refactor and make common
-
-                            if (checkForRemoteOperationError(synchResult)) {
-
-                                requestCredentialsUpdate(context);
-
-                            } else if (RemoteOperationResult.ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED.equals(
-                                    synchResult.getCode())) {
-
-                                showUntrustedCertDialog(synchResult);
+                                synchResult != null) {
+
+                            if (synchResult.isSuccess()) {
+                                hideInfoBox();
+                            } else {
+                                // TODO refactor and make common
+                                if (checkForRemoteOperationError(synchResult)) {
+                                    requestCredentialsUpdate(context);
+                                } else if (RemoteOperationResult.ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED.equals(
+                                        synchResult.getCode())) {
+                                    showUntrustedCertDialog(synchResult);
+                                } else if (ResultCode.MAINTENANCE_MODE.equals(synchResult.getCode())) {
+                                    showInfoBox(R.string.maintenance_mode);
+                                } else if (ResultCode.NO_NETWORK_CONNECTION.equals(synchResult.getCode()) ||
+                                        ResultCode.HOST_NOT_AVAILABLE.equals(synchResult.getCode())) {
+                                    showInfoBox(R.string.offline_mode);
+                                }
                             }
-
-
                         }
                         removeStickyBroadcast(intent);
                         DataHolderUtil.getInstance().delete(intent.getStringExtra(FileSyncAdapter.EXTRA_RESULT));
@@ -1384,8 +1422,8 @@ public class FileDisplayActivity extends HookActivity
                 String accountName = intent.getStringExtra(FileUploader.ACCOUNT_NAME);
                 boolean sameAccount = getAccount() != null && accountName.equals(getAccount().name);
                 OCFile currentDir = getCurrentDir();
-                boolean isDescendant = (currentDir != null) && (uploadedRemotePath != null) &&
-                        (uploadedRemotePath.startsWith(currentDir.getRemotePath()));
+                boolean isDescendant = currentDir != null && uploadedRemotePath != null &&
+                        uploadedRemotePath.startsWith(currentDir.getRemotePath());
 
                 if (sameAccount && isDescendant) {
                     String linkedToRemotePath =
@@ -1463,7 +1501,7 @@ public class FileDisplayActivity extends HookActivity
 
     /**
      * Class waiting for broadcast events from the {@link FileDownloader} service.
-     * <p/>
+     *
      * Updates the UI when a download is started or finished, provided that it is relevant for the
      * current folder.
      */
@@ -1489,8 +1527,9 @@ public class FileDisplayActivity extends HookActivity
                 }
 
                 if (mWaitingToSend != null) {
-                    mWaitingToSend = getStorageManager().getFileByPath(mWaitingToSend.getRemotePath());
-                    if (mWaitingToSend.isDown() && downloadBehaviour != null) {
+                    // update file after downloading
+                    mWaitingToSend = getStorageManager().getFileByRemoteId(mWaitingToSend.getRemoteId());
+                    if (mWaitingToSend != null && mWaitingToSend.isDown() && downloadBehaviour != null) {
                         switch (downloadBehaviour) {
                             case OCFileListFragment.DOWNLOAD_SEND:
                                 String packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME);
@@ -1531,8 +1570,7 @@ public class FileDisplayActivity extends HookActivity
 
         private boolean isSameAccount(Intent intent) {
             String accountName = intent.getStringExtra(FileDownloader.ACCOUNT_NAME);
-            return (accountName != null && getAccount() != null &&
-                    accountName.equals(getAccount().name));
+            return accountName != null && getAccount() != null && accountName.equals(getAccount().name);
         }
     }
 
@@ -1726,6 +1764,8 @@ public class FileDisplayActivity extends HookActivity
             onUpdateShareInformation(result, R.string.updating_share_failed);
         } else if (operation instanceof UnshareOperation) {
             onUpdateShareInformation(result, R.string.unsharing_failed);
+        } else if (operation instanceof RestoreFileVersionOperation) {
+            onRestoreFileVersionOperationFinish(result);
         }
     }
 
@@ -1758,15 +1798,21 @@ public class FileDisplayActivity extends HookActivity
      */
     private void onRemoveFileOperationFinish(RemoveFileOperation operation,
                                              RemoteOperationResult result) {
-        DisplayUtils.showSnackMessage(
-                this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources())
-        );
+
+        if (!operation.isInBackground()) {
+            DisplayUtils.showSnackMessage(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation,
+                    getResources()));
+        }
 
         if (result.isSuccess()) {
             OCFile removedFile = operation.getFile();
             tryStopPlaying(removedFile);
             FileFragment second = getSecondFragment();
-            if (second != null && removedFile.equals(second.getFile())) {
+
+            // check if file is still available, if so do nothing
+            boolean fileAvailable = getStorageManager().fileExists(removedFile.getFileId());
+
+            if (second != null && !fileAvailable && removedFile.equals(second.getFile())) {
                 if (second instanceof PreviewMediaFragment) {
                     ((PreviewMediaFragment) second).stopPreview(true);
                 }
@@ -1785,6 +1831,34 @@ public class FileDisplayActivity extends HookActivity
         }
     }
 
+    private void onRestoreFileVersionOperationFinish(RemoteOperationResult result) {
+        if (result.isSuccess()) {
+            OCFile file = getFile();
+
+            // delete old local copy
+            if (file.isDown()) {
+                List<OCFile> list = new ArrayList<>();
+                list.add(file);
+                getFileOperationsHelper().removeFiles(list, true, true);
+
+                // download new version, only if file was previously download
+                getFileOperationsHelper().syncFile(file);
+            }
+
+            OCFile parent = getStorageManager().getFileById(file.getParentId());
+            startSyncFolderOperation(parent, true, true);
+
+            if (getSecondFragment() instanceof FileDetailFragment) {
+                FileDetailFragment fileDetailFragment = (FileDetailFragment) getSecondFragment();
+                fileDetailFragment.getFileDetailActivitiesFragment().reload();
+            }
+
+            DisplayUtils.showSnackMessage(this, R.string.file_version_restored_successfully);
+        } else {
+            DisplayUtils.showSnackMessage(this, R.string.file_version_restored_error);
+        }
+    }
+
     public void setMediaServiceConnection() {
         mMediaServiceConnection = newMediaConnection();// mediaServiceConnection;
         bindService(new Intent(this, MediaService.class), mMediaServiceConnection, Context.BIND_AUTO_CREATE);
@@ -1877,7 +1951,10 @@ public class FileDisplayActivity extends HookActivity
             DialogFragment chooserDialog = ShareLinkToDialog.newInstance(intentToShareLink, packagesToExclude);
             chooserDialog.show(getSupportFragmentManager(), FTAG_CHOOSER_DIALOG);
 
-            fileDetailFragment.getFileDetailSharingFragment().refreshPublicShareFromDB();
+            if (fileDetailFragment != null && fileDetailFragment.getFileDetailSharingFragment() != null) {
+                fileDetailFragment.getFileDetailSharingFragment().refreshPublicShareFromDB();
+                fileDetailFragment.getFileDetailSharingFragment().onUpdateShareInformation(result, getFile());
+            }
             refreshListOfFilesFragment(false);
         } else {
             // Detect Failure (403) --> maybe needs password
@@ -1895,7 +1972,9 @@ public class FileDisplayActivity extends HookActivity
                 }
 
             } else {
-                fileDetailFragment.getFileDetailSharingFragment().refreshPublicShareFromDB();
+                if (fileDetailFragment != null && fileDetailFragment.getFileDetailSharingFragment() != null) {
+                    fileDetailFragment.getFileDetailSharingFragment().refreshPublicShareFromDB();
+                }
                 Snackbar.make(
                         findViewById(android.R.id.content),
                         ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()),
@@ -1975,7 +2054,9 @@ public class FileDisplayActivity extends HookActivity
                     ((PreviewMediaFragment) details).updateFile(renamedFile);
                     if (PreviewMediaFragment.canBePreviewed(renamedFile)) {
                         int position = ((PreviewMediaFragment) details).getPosition();
-                        startMediaPreview(renamedFile, position, true, true);
+                        boolean streaming = AccountUtils.getServerVersionForAccount(getAccount(), this)
+                                .isMediaStreamingSupported();
+                        startMediaPreview(renamedFile, position, true, true, streaming);
                     } else {
                         getFileOperationsHelper().openFile(renamedFile);
                     }
@@ -2027,13 +2108,18 @@ public class FileDisplayActivity extends HookActivity
     private void onCreateFolderOperationFinish(CreateFolderOperation operation,
                                                RemoteOperationResult result) {
         if (result.isSuccess()) {
-            refreshListOfFilesFragment(false);
+            OCFileListFragment fileListFragment = getListOfFilesFragment();
+            if (fileListFragment != null) {
+                fileListFragment.onItemClicked(getStorageManager().getFileByPath(operation.getRemotePath()));
+            }
         } else {
             try {
-                DisplayUtils.showSnackMessage(
-                        this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources())
-                );
-
+                if (ResultCode.FOLDER_ALREADY_EXISTS == result.getCode()) {
+                    DisplayUtils.showSnackMessage(this, R.string.folder_already_exists);
+                } else {
+                    DisplayUtils.showSnackMessage(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation,
+                            getResources()));
+                }
             } catch (NotFoundException e) {
                 Log_OC.e(TAG, "Error while trying to show fail message ", e);
             }
@@ -2082,9 +2168,9 @@ public class FileDisplayActivity extends HookActivity
 
     /**
      * Starts an operation to refresh the requested folder.
-     * <p/>
+     *
      * The operation is run in a new background thread created on the fly.
-     * <p/>
+     *
      * The refresh updates is a "light sync": properties of regular files in folder are updated (including
      * associated shares), but not their contents. Only the contents of files marked to be kept-in-sync are
      * synchronized too.
@@ -2093,7 +2179,25 @@ public class FileDisplayActivity extends HookActivity
      * @param ignoreETag If 'true', the data from the server will be fetched and sync'ed even if the eTag
      *                   didn't change.
      */
-    public void startSyncFolderOperation(final OCFile folder, final boolean ignoreETag) {
+    public void startSyncFolderOperation(OCFile folder, boolean ignoreETag) {
+        startSyncFolderOperation(folder, ignoreETag, false);
+    }
+
+    /**
+     * Starts an operation to refresh the requested folder.
+     *
+     * The operation is run in a new background thread created on the fly.
+     *
+     * The refresh updates is a "light sync": properties of regular files in folder are updated (including
+     * associated shares), but not their contents. Only the contents of files marked to be kept-in-sync are
+     * synchronized too.
+     *
+     * @param folder      Folder to refresh.
+     * @param ignoreETag  If 'true', the data from the server will be fetched and sync'ed even if the eTag
+     *                    didn't change.
+     * @param ignoreFocus reloads file list even without focus, e.g. on tablet mode, focus can still be in detail view
+     */
+    public void startSyncFolderOperation(final OCFile folder, final boolean ignoreETag, boolean ignoreFocus) {
 
         // the execution is slightly delayed to allow the activity get the window focus if it's being started
         // or if the method is called from a dialog that is being dismissed
@@ -2102,7 +2206,7 @@ public class FileDisplayActivity extends HookActivity
                     new Runnable() {
                         @Override
                         public void run() {
-                            if (hasWindowFocus()) {
+                            if (ignoreFocus || hasWindowFocus()) {
                                 long currentSyncTime = System.currentTimeMillis();
                                 mSyncInProgress = true;
 
@@ -2110,7 +2214,6 @@ public class FileDisplayActivity extends HookActivity
                                 RemoteOperation synchFolderOp = new RefreshFolderOperation(folder,
                                         currentSyncTime,
                                         false,
-                                        getFileOperationsHelper().isSharedSupported(),
                                         ignoreETag,
                                         getStorageManager(),
                                         getAccount(),
@@ -2153,7 +2256,7 @@ public class FileDisplayActivity extends HookActivity
     private void sendDownloadedFile(String packageName, String activityName) {
         if (mWaitingToSend != null) {
             Intent sendIntent = new Intent(Intent.ACTION_SEND);
-            sendIntent.setType(mWaitingToSend.getMimetype());
+            sendIntent.setType(mWaitingToSend.getMimeType());
             sendIntent.putExtra(Intent.EXTRA_STREAM, mWaitingToSend.getExposedFileUri(this));
             sendIntent.putExtra(Intent.ACTION_SEND, true);
 
@@ -2182,7 +2285,7 @@ public class FileDisplayActivity extends HookActivity
                                         String activityName) {
         mWaitingToSend = file;
         requestForDownload(mWaitingToSend, downloadBehaviour, packageName, activityName);
-        boolean hasSecondFragment = (getSecondFragment() != null);
+        boolean hasSecondFragment = getSecondFragment() != null;
         updateFragmentsVisibility(hasSecondFragment);
     }
 
@@ -2231,8 +2334,9 @@ public class FileDisplayActivity extends HookActivity
      * @param autoplay              When 'true', the playback will start without user
      *                              interactions.
      */
-    public void startMediaPreview(OCFile file, int startPlaybackPosition, boolean autoplay, boolean showPreview) {
-        if (showPreview && file.isDown() && !file.isDownloading()) {
+    public void startMediaPreview(OCFile file, int startPlaybackPosition, boolean autoplay, boolean showPreview,
+                                  boolean streamMedia) {
+        if (showPreview && file.isDown() && !file.isDownloading() || streamMedia) {
             Fragment mediaFragment = PreviewMediaFragment.newInstance(file, getAccount(), startPlaybackPosition, autoplay);
             setSecondFragment(mediaFragment);
             updateFragmentsVisibility(true);
@@ -2339,11 +2443,9 @@ public class FileDisplayActivity extends HookActivity
 
     private void refreshList(boolean ignoreETag) {
         OCFileListFragment listOfFiles = getListOfFilesFragment();
-        if (listOfFiles != null && !listOfFiles.getIsSearchFragment()) {
+        if (listOfFiles != null && !listOfFiles.isSearchFragment()) {
             OCFile folder = listOfFiles.getCurrentFile();
             if (folder != null) {
-                /*mFile = mContainerActivity.getStorageManager().getFileById(mFile.getFileId());
-                listDirectory(mFile);*/
                 startSyncFolderOperation(folder, ignoreETag);
             }
         }
@@ -2369,9 +2471,11 @@ public class FileDisplayActivity extends HookActivity
         if (event.getIntent().getBooleanExtra(TEXT_PREVIEW, false)) {
             startTextPreview((OCFile) bundle.get(EXTRA_FILE), true);
         } else if (bundle.containsKey(PreviewVideoActivity.EXTRA_START_POSITION)) {
+            boolean streaming = AccountUtils.getServerVersionForAccount(getAccount(), this)
+                    .isMediaStreamingSupported();
             startMediaPreview((OCFile)bundle.get(EXTRA_FILE),
                     (int)bundle.get(PreviewVideoActivity.EXTRA_START_POSITION),
-                    (boolean)bundle.get(PreviewVideoActivity.EXTRA_AUTOPLAY), true);
+                    (boolean) bundle.get(PreviewVideoActivity.EXTRA_AUTOPLAY), true, streaming);
         } else if (bundle.containsKey(PreviewImageActivity.EXTRA_VIRTUAL_TYPE)) {
             startImagePreview((OCFile)bundle.get(EXTRA_FILE),
                     (VirtualFolderType)bundle.get(PreviewImageActivity.EXTRA_VIRTUAL_TYPE),

+ 7 - 8
src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.java

@@ -78,10 +78,10 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C
 
     private static final String TAG_LIST_OF_FOLDERS = "LIST_OF_FOLDERS";
        
-    private boolean mSyncInProgress = false;
+    private boolean mSyncInProgress;
 
-    private boolean mSearchOnlyFolders = false;
-    private boolean mDoNotEnterEncryptedFolder = false;
+    private boolean mSearchOnlyFolders;
+    private boolean mDoNotEnterEncryptedFolder;
 
     protected Button mCancelBtn;
     protected Button mChooseBtn;
@@ -250,8 +250,7 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C
                 
         // perform folder synchronization
         RemoteOperation refreshFolderOperation = new RefreshFolderOperation(folder, currentSyncTime, false,
-                getFileOperationsHelper().isSharedSupported(), ignoreETag, getStorageManager(), getAccount(),
-                getApplicationContext());
+                ignoreETag, getStorageManager(), getAccount(), getApplicationContext());
 
         refreshFolderOperation.execute(getAccount(), this, null, null);
         setIndeterminate(true);
@@ -380,7 +379,7 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C
         ActionBar actionBar = getSupportActionBar();
 
         if (actionBar != null) {
-            boolean atRoot = (currentDir == null || currentDir.getParentId() == 0);
+            boolean atRoot = currentDir == null || currentDir.getParentId() == 0;
             actionBar.setDisplayHomeAsUpEnabled(!atRoot);
             actionBar.setHomeButtonEnabled(!atRoot);
 
@@ -471,8 +470,8 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C
                 String syncFolderRemotePath = intent.getStringExtra(FileSyncAdapter.EXTRA_FOLDER_PATH);
                 RemoteOperationResult syncResult = (RemoteOperationResult)
                         DataHolderUtil.getInstance().retrieve(intent.getStringExtra(FileSyncAdapter.EXTRA_RESULT));
-                boolean sameAccount = (getAccount() != null && accountName.equals(getAccount().name)
-                        && getStorageManager() != null); 
+                boolean sameAccount = getAccount() != null && accountName.equals(getAccount().name)
+                        && getStorageManager() != null;
     
                 if (sameAccount) {
                     if (FileSyncAdapter.EVENT_FULL_SYNC_START.equals(event)) {

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů