Browse Source

Merge branch 'dark_theme' of github.com:nextcloud/android into dark_theme

Signed-off-by: Daniel Bailey <daniel.bailey@grappleIT.co.uk>
Daniel Bailey 5 years ago
parent
commit
9a9e0e6c42
100 changed files with 2328 additions and 1463 deletions
  1. 13 1
      .idea/codeStyles/Project.xml
  2. 13 0
      CHANGELOG.md
  3. 4 2
      CONTRIBUTING.md
  4. 26 10
      build.gradle
  5. 1 1
      scripts/analysis/findbugs-results.txt
  6. 11 0
      spotbugs-filter.xml
  7. 9 9
      src/main/AndroidManifest.xml
  8. 3 1
      src/main/java/com/nextcloud/client/di/AppComponent.java
  9. 6 1
      src/main/java/com/nextcloud/client/di/AppModule.java
  10. 0 2
      src/main/java/com/nextcloud/client/di/ComponentsModule.java
  11. 81 0
      src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt
  12. 37 0
      src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt
  13. 54 0
      src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt
  14. 85 0
      src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt
  15. 50 0
      src/main/java/com/nextcloud/client/jobs/JobsModule.kt
  16. 1 1
      src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.java
  17. 8 1
      src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java
  18. 20 4
      src/main/java/com/owncloud/android/MainApp.java
  19. 48 21
      src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java
  20. 7 0
      src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.java
  21. 7 1
      src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  22. 11 3
      src/main/java/com/owncloud/android/datamodel/MediaProvider.java
  23. 1 1
      src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java
  24. 4 1
      src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java
  25. 96 65
      src/main/java/com/owncloud/android/jobs/AccountRemovalJob.java
  26. 3 15
      src/main/java/com/owncloud/android/jobs/FilesSyncJob.java
  27. 13 0
      src/main/java/com/owncloud/android/jobs/MediaFoldersDetectionJob.java
  28. 0 102
      src/main/java/com/owncloud/android/jobs/NContentObserverJob.java
  29. 39 39
      src/main/java/com/owncloud/android/operations/DetectAuthenticationMethodOperation.java
  30. 4 0
      src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java
  31. 368 214
      src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java
  32. 2 1
      src/main/java/com/owncloud/android/providers/FileContentProvider.java
  33. 0 1
      src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java
  34. 7 28
      src/main/java/com/owncloud/android/ui/activity/BaseActivity.java
  35. 2 2
      src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java
  36. 0 1
      src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
  37. 36 30
      src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java
  38. 46 53
      src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  39. 6 17
      src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.java
  40. 8 4
      src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java
  41. 4 2
      src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.java
  42. 4 3
      src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java
  43. 9 2
      src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java
  44. 3 2
      src/main/java/com/owncloud/android/ui/activity/ShareActivity.java
  45. 9 2
      src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java
  46. 2 2
      src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java
  47. 10 14
      src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java
  48. 0 77
      src/main/java/com/owncloud/android/ui/activity/UploadPathActivity.java
  49. 1 1
      src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java
  50. 12 5
      src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java
  51. 4 3
      src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java
  52. 4 0
      src/main/java/com/owncloud/android/ui/asynctasks/FetchRemoteFileTask.java
  53. 7 2
      src/main/java/com/owncloud/android/ui/asynctasks/NotificationExecuteActionTask.java
  54. 2 4
      src/main/java/com/owncloud/android/ui/asynctasks/PrintAsyncTask.java
  55. 0 95
      src/main/java/com/owncloud/android/ui/components/CustomEditText.java
  56. 7 2
      src/main/java/com/owncloud/android/ui/dialog/NoteDialogFragment.java
  57. 5 3
      src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java
  58. 17 3
      src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java
  59. 2 2
      src/main/java/com/owncloud/android/ui/fragment/PhotoFragment.java
  60. 6 0
      src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactListFragment.java
  61. 4 1
      src/main/java/com/owncloud/android/ui/notifications/NotificationsContract.java
  62. 28 33
      src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.java
  63. 0 1
      src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java
  64. 65 6
      src/main/java/com/owncloud/android/ui/preview/PreviewTextFragment.java
  65. 2 2
      src/main/java/com/owncloud/android/ui/preview/PreviewVideoActivity.java
  66. 9 7
      src/main/java/com/owncloud/android/utils/FileStorageUtils.java
  67. 3 32
      src/main/java/com/owncloud/android/utils/FilesSyncHelper.java
  68. 6 5
      src/main/java/com/owncloud/android/utils/MimeTypeUtil.java
  69. 33 3
      src/main/java/org/nextcloud/providers/cursors/FileCursor.java
  70. 10 10
      src/main/java/org/nextcloud/providers/cursors/RootCursor.java
  71. 227 257
      src/main/res/layout-land/account_setup.xml
  72. 229 239
      src/main/res/layout/account_setup.xml
  73. 82 0
      src/main/res/values-ar/strings.xml
  74. 2 0
      src/main/res/values-ca/strings.xml
  75. 2 0
      src/main/res/values-da/strings.xml
  76. 3 0
      src/main/res/values-de/strings.xml
  77. 37 0
      src/main/res/values-el/strings.xml
  78. 3 0
      src/main/res/values-es/strings.xml
  79. 4 0
      src/main/res/values-eu/strings.xml
  80. 4 0
      src/main/res/values-fi-rFI/strings.xml
  81. 3 0
      src/main/res/values-fr/strings.xml
  82. 3 0
      src/main/res/values-gl/strings.xml
  83. 3 0
      src/main/res/values-it/strings.xml
  84. 1 0
      src/main/res/values-ja-rJP/strings.xml
  85. 2 0
      src/main/res/values-nl/strings.xml
  86. 4 1
      src/main/res/values-pl/strings.xml
  87. 3 0
      src/main/res/values-pt-rBR/strings.xml
  88. 9 0
      src/main/res/values-ro/strings.xml
  89. 5 2
      src/main/res/values-ru/strings.xml
  90. 3 0
      src/main/res/values-sk-rSK/strings.xml
  91. 32 0
      src/main/res/values-sl/strings.xml
  92. 2 0
      src/main/res/values-sr/strings.xml
  93. 3 0
      src/main/res/values-sv/strings.xml
  94. 3 0
      src/main/res/values-tr/strings.xml
  95. 0 5
      src/main/res/values-v21/styles.xml
  96. 13 0
      src/main/res/values-zh-rTW/strings.xml
  97. 0 3
      src/main/res/values/setup.xml
  98. 3 0
      src/main/res/values/strings.xml
  99. 107 0
      src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt
  100. 138 0
      src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt

+ 13 - 1
.idea/codeStyles/Project.xml

@@ -27,6 +27,9 @@
         <emptyLine />
       </value>
     </option>
+    <AndroidXmlCodeStyleSettings>
+      <option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
+    </AndroidXmlCodeStyleSettings>
     <JavaCodeStyleSettings>
       <option name="IMPORT_LAYOUT_TABLE">
         <value>
@@ -86,6 +89,7 @@
               <match>
                 <AND>
                   <NAME>xmlns:android</NAME>
+                  <XML_ATTRIBUTE />
                   <XML_NAMESPACE>^$</XML_NAMESPACE>
                 </AND>
               </match>
@@ -96,6 +100,7 @@
               <match>
                 <AND>
                   <NAME>xmlns:.*</NAME>
+                  <XML_ATTRIBUTE />
                   <XML_NAMESPACE>^$</XML_NAMESPACE>
                 </AND>
               </match>
@@ -107,6 +112,7 @@
               <match>
                 <AND>
                   <NAME>.*:id</NAME>
+                  <XML_ATTRIBUTE />
                   <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                 </AND>
               </match>
@@ -117,6 +123,7 @@
               <match>
                 <AND>
                   <NAME>.*:name</NAME>
+                  <XML_ATTRIBUTE />
                   <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                 </AND>
               </match>
@@ -127,6 +134,7 @@
               <match>
                 <AND>
                   <NAME>name</NAME>
+                  <XML_ATTRIBUTE />
                   <XML_NAMESPACE>^$</XML_NAMESPACE>
                 </AND>
               </match>
@@ -137,6 +145,7 @@
               <match>
                 <AND>
                   <NAME>style</NAME>
+                  <XML_ATTRIBUTE />
                   <XML_NAMESPACE>^$</XML_NAMESPACE>
                 </AND>
               </match>
@@ -147,6 +156,7 @@
               <match>
                 <AND>
                   <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
                   <XML_NAMESPACE>^$</XML_NAMESPACE>
                 </AND>
               </match>
@@ -158,6 +168,7 @@
               <match>
                 <AND>
                   <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
                   <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                 </AND>
               </match>
@@ -169,6 +180,7 @@
               <match>
                 <AND>
                   <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
                   <XML_NAMESPACE>.*</XML_NAMESPACE>
                 </AND>
               </match>
@@ -179,4 +191,4 @@
       </arrangement>
     </codeStyleSettings>
   </code_scheme>
-</component>
+</component>

+ 13 - 0
CHANGELOG.md

@@ -1,3 +1,16 @@
+## 3.8.1 (October, 11, 2019)
+
+- upload images into subfolder, if source folder also has subfolder
+- Fix registration of second account on first run
+- fix disappearing account list
+- fix recurring synced folder notification
+- fix vanishing images
+- auto upload: fix relative paths
+- bugfix release
+- updated translations
+
+For a full list, please see https://github.com/nextcloud/android/milestone/39
+
 ## 3.8.0 (September, 14, 2019)
 
 - FIDO U2F support on login

+ 4 - 2
CONTRIBUTING.md

@@ -122,7 +122,7 @@ There are three build variants
 ### 2. Create pull request:
 * Commit your changes locally: ```git commit -a```
 * Push your changes to your GitHub repo: ```git push```
-* Browse to https://github.com/YOURGITHUBNAME/android/pulls and issue pull request
+* Browse to <https://github.com/YOURGITHUBNAME/android/pulls> and issue pull request
 * Enter description and send pull request.
 
 
@@ -294,7 +294,9 @@ For dev the version name is in format YYYYMMDD. It is mainly as a reference for
 * 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).
+* Bugfix releases (dot releases, e.g. 3.2.1) are released 4 weeks after stable version 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).
+
+> Hotfixes as well as security fixes are released via bugfix releases (dot releases) but are released on demand in contrast to regular, scheduled bugfix releases.
 
 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)
 

+ 26 - 10
build.gradle

@@ -26,7 +26,7 @@ buildscript {
         }
         classpath 'gradle.plugin.com.github.spotbugs:spotbugs-gradle-plugin:1.6.6'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
-        classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.0.1"
+        classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.1.1"
     }
 }
 
@@ -48,6 +48,7 @@ configurations {
         exclude group: 'com.google.firebase', module: 'firebase-core'
         exclude group: 'com.google.firebase', module: 'firebase-analytics'
         exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
+        exclude group: 'org.jetbrains', module: 'annotations-java5' // via prism4j, already using annotations explicitely
 
         // check for updates every build
         resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
@@ -56,7 +57,9 @@ configurations {
 
 ext {
     jacocoVersion = "0.8.2"
-    daggerVersion = "2.24"
+    daggerVersion = "2.25.2"
+    markwonVersion =  "4.1.2"
+    prismVersion = "2.0.0"
     androidLibraryVersion = "master-SNAPSHOT"
 
     travisBuild = System.getenv("TRAVIS") == "true"
@@ -159,8 +162,8 @@ android {
             versionDev {
                 applicationId "com.nextcloud.android.beta"
                 dimension "default"
-                versionCode 20191005
-                versionName "20191005"
+                versionCode 20191026
+                versionName "20191026"
             }
 
             qa {
@@ -272,6 +275,8 @@ dependencies {
     implementation 'androidx.cardview:cardview:1.0.0'
     implementation 'androidx.exifinterface:exifinterface:1.0.0'
     implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0"
+    implementation "androidx.work:work-runtime:2.2.0"
+    implementation "androidx.work:work-runtime-ktx:2.2.0"
     implementation 'com.github.albfernandez:juniversalchardet:2.0.3' // need this version for Android <7
     implementation 'com.google.code.findbugs:annotations:2.0.1'
     implementation 'commons-io:commons-io:2.6'
@@ -300,8 +305,8 @@ dependencies {
 
     implementation 'com.github.cotechde.hwsecurity:hwsecurity-fido:2.5.1'
 
-    spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.9.0'
-    spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.4.6'
+    spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.10.0'
+    spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.4.7'
 
     implementation "com.google.dagger:dagger:$daggerVersion"
     implementation "com.google.dagger:dagger-android:$daggerVersion"
@@ -312,16 +317,27 @@ dependencies {
     compileOnly "org.projectlombok:lombok:1.18.10"
     annotationProcessor "org.projectlombok:lombok:1.18.10"
 
-    ktlint "com.pinterest:ktlint:0.34.2"
+    ktlint "com.pinterest:ktlint:0.35.0"
     implementation 'org.conscrypt:conscrypt-android:2.2.1'
 
+    // dependencies for markdown rendering
+    implementation "io.noties.markwon:core:$markwonVersion"
+    implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
+    implementation "io.noties.markwon:ext-tables:$markwonVersion"
+    implementation "io.noties.markwon:ext-tasklist:$markwonVersion"
+    implementation "io.noties.markwon:html:$markwonVersion"
+
+    implementation "io.noties.markwon:syntax-highlight:$markwonVersion"
+    implementation "io.noties:prism4j:$prismVersion"
+    kapt "io.noties:prism4j-bundler:$prismVersion"
+
     // dependencies for local unit tests
     testImplementation 'junit:junit:4.12'
     testImplementation 'org.mockito:mockito-core:3.1.0'
     testImplementation 'androidx.test:core:1.2.0'
-    testImplementation 'org.powermock:powermock-core:2.0.2'
-    testImplementation 'org.powermock:powermock-module-junit4:2.0.2'
-    testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
+    testImplementation 'org.powermock:powermock-core:2.0.4'
+    testImplementation 'org.powermock:powermock-module-junit4:2.0.4'
+    testImplementation 'org.powermock:powermock-api-mockito2:2.0.4'
     testImplementation 'org.json:json:20190722'
     testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
     testImplementation "androidx.arch.core:core-testing:2.1.0"

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

@@ -1 +1 @@
-427
+420

+ 11 - 0
spotbugs-filter.xml

@@ -34,6 +34,17 @@
         </Or>
     </Match>
 
+	<!-- Third-party library code -->
+	<Match>
+	    <Or>
+	        <Package name="~io\.noties\..*" />
+			<Package name="~third_parties\.ezvcard_android\..*" />
+		</Or>
+	</Match>
+	 <Match>
+         <Class name="~com\.owncloud\.android\.ui\.preview\.MarkwonGrammarLocator.*" />
+    </Match>
+
     <Bug pattern="PATH_TRAVERSAL_IN" />
     <Bug pattern="ANDROID_EXTERNAL_FILE_ACCESS" />
     <Bug pattern="BAS_BLOATED_ASSIGNMENT_SCOPE" />

+ 9 - 9
src/main/AndroidManifest.xml

@@ -72,7 +72,7 @@
     must request the FOREGROUND_SERVICE permission -->
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
 
-    <uses-sdk tools:overrideLibrary="com.blikoon.qrcodescanner" />
+    <uses-sdk tools:overrideLibrary="com.blikoon.qrcodescanner, io.noties.markwon.ext.latex, io.noties.markwon.ext.strikethrough, io.noties.markwon.ext.tables, io.noties.markwon.ext.tasklist, io.noties.markwon.html, io.noties.markwon.linkify, io.noties.markwon.syntax, io.noties.markwon, ru.noties.jlatexmath.android" />
 
     <!-- Some Chromebooks don't support touch. Although not essential,
          it's a good idea to explicitly include this declaration. -->
@@ -164,11 +164,6 @@
             android:label="@string/app_name"
             android:theme="@style/Theme.ownCloud.Fullscreen" />
 
-        <service
-            android:name=".jobs.NContentObserverJob"
-            android:permission="android.permission.BIND_JOB_SERVICE" >
-        </service>
-
         <service
             android:name=".authentication.AccountAuthenticatorService"
             android:exported="true" >
@@ -272,6 +267,14 @@
             android:exported="true">
         </provider>
 
+        <!-- Disable WorkManager initialization. Whoever designed this, should pay closer attention -->
+        <!-- to "best before" dates in his fridge. -->
+        <provider
+            android:name="androidx.work.impl.WorkManagerInitializer"
+            android:authorities=".workmanager-init"
+            android:exported="false"
+            tools:node="remove" />
+
         <activity
             android:name=".authentication.AuthenticatorActivity"
             android:exported="true"
@@ -360,9 +363,6 @@
             android:theme="@style/Theme.ownCloud.Dialog.NoTitle"
             android:launchMode="singleTop"
             android:windowSoftInputMode="adjustResize"/>
-        <activity
-            android:name=".ui.activity.UploadPathActivity"
-            android:label="@string/app_name" />
         <activity
             android:name=".ui.activity.ShareActivity"
             android:label="@string/share_dialog_title"

+ 3 - 1
src/main/java/com/nextcloud/client/di/AppComponent.java

@@ -24,6 +24,7 @@ import android.app.Application;
 
 import com.nextcloud.client.appinfo.AppInfoModule;
 import com.nextcloud.client.device.DeviceModule;
+import com.nextcloud.client.jobs.JobsModule;
 import com.nextcloud.client.network.NetworkModule;
 import com.nextcloud.client.onboarding.OnboardingModule;
 import com.nextcloud.client.preferences.PreferencesModule;
@@ -43,7 +44,8 @@ import dagger.android.support.AndroidSupportInjectionModule;
     NetworkModule.class,
     DeviceModule.class,
     OnboardingModule.class,
-    ViewModelModule.class
+    ViewModelModule.class,
+    JobsModule.class
 })
 @Singleton
 public interface AppComponent {

+ 6 - 1
src/main/java/com/nextcloud/client/di/AppModule.java

@@ -33,9 +33,9 @@ import com.nextcloud.client.account.CurrentAccountProvider;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.account.UserAccountManagerImpl;
 import com.nextcloud.client.core.AsyncRunner;
-import com.nextcloud.client.core.ThreadPoolAsyncRunner;
 import com.nextcloud.client.core.Clock;
 import com.nextcloud.client.core.ClockImpl;
+import com.nextcloud.client.core.ThreadPoolAsyncRunner;
 import com.nextcloud.client.device.DeviceInfo;
 import com.nextcloud.client.logger.FileLogHandler;
 import com.nextcloud.client.logger.Logger;
@@ -71,6 +71,11 @@ class AppModule {
         return application;
     }
 
+    @Provides
+    ContentResolver contentResolver(Context context) {
+        return context.getContentResolver();
+    }
+
     @Provides
     Resources resources(Application application) {
         return application.getResources();

+ 0 - 2
src/main/java/com/nextcloud/client/di/ComponentsModule.java

@@ -60,7 +60,6 @@ import com.owncloud.android.ui.activity.SsoGrantPermissionActivity;
 import com.owncloud.android.ui.activity.SyncedFoldersActivity;
 import com.owncloud.android.ui.activity.UploadFilesActivity;
 import com.owncloud.android.ui.activity.UploadListActivity;
-import com.owncloud.android.ui.activity.UploadPathActivity;
 import com.owncloud.android.ui.activity.UserInfoActivity;
 import com.owncloud.android.ui.dialog.ChooseTemplateDialogFragment;
 import com.owncloud.android.ui.dialog.MultipleAccountsDialog;
@@ -122,7 +121,6 @@ abstract class ComponentsModule {
     @ContributesAndroidInjector abstract TrashbinActivity trashbinActivity();
     @ContributesAndroidInjector abstract UploadFilesActivity uploadFilesActivity();
     @ContributesAndroidInjector abstract UploadListActivity uploadListActivity();
-    @ContributesAndroidInjector abstract UploadPathActivity uploadPathActivity();
     @ContributesAndroidInjector abstract UserInfoActivity userInfoActivity();
     @ContributesAndroidInjector abstract WhatsNewActivity whatsNewActivity();
     @ContributesAndroidInjector abstract EtmActivity etmActivity();

+ 81 - 0
src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt

@@ -0,0 +1,81 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * 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.nextcloud.client.jobs
+
+import android.content.ContentResolver
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.work.ListenableWorker
+import androidx.work.WorkerFactory
+import androidx.work.WorkerParameters
+import com.nextcloud.client.device.DeviceInfo
+import com.nextcloud.client.device.PowerManagementService
+import com.nextcloud.client.preferences.AppPreferences
+import com.owncloud.android.datamodel.SyncedFolderProvider
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * This factory is responsible for creating all background jobs and for injecting
+ * all jobs dependencies.
+ */
+class BackgroundJobFactory @Inject constructor(
+    private val preferences: AppPreferences,
+    private val contentResolver: ContentResolver,
+    private val powerManagerService: PowerManagementService,
+    private val backgroundJobManager: Provider<BackgroundJobManager>,
+    private val deviceInfo: DeviceInfo
+) : WorkerFactory() {
+
+    override fun createWorker(
+        context: Context,
+        workerClassName: String,
+        workerParameters: WorkerParameters
+    ): ListenableWorker? {
+
+        val workerClass = try {
+            Class.forName(workerClassName).kotlin
+        } catch (ex: ClassNotFoundException) {
+            null
+        }
+
+        return when (workerClass) {
+            ContentObserverWork::class -> createContentObserverJob(context, workerParameters)
+            else -> null // falls back to default factory
+        }
+    }
+
+    private fun createContentObserverJob(context: Context, workerParameters: WorkerParameters): ListenableWorker? {
+        val folderResolver = SyncedFolderProvider(contentResolver, preferences)
+        @RequiresApi(Build.VERSION_CODES.N)
+        if (deviceInfo.apiLevel >= Build.VERSION_CODES.N) {
+            return ContentObserverWork(
+                context,
+                workerParameters,
+                folderResolver,
+                powerManagerService,
+                backgroundJobManager.get()
+            )
+        } else {
+            return null
+        }
+    }
+}

+ 37 - 0
src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt

@@ -0,0 +1,37 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * 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.nextcloud.client.jobs
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+/**
+ * This interface allows to control, schedule and monitor all application
+ * long-running background tasks, such as periodic checks or synchronization.
+ */
+interface BackgroundJobManager {
+
+    /**
+     * Start content observer job that monitors changes in media folders
+     * and launches synchronization when needed.
+     */
+    @RequiresApi(Build.VERSION_CODES.N)
+    fun scheduleContentObserverJob()
+}

+ 54 - 0
src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt

@@ -0,0 +1,54 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * 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.nextcloud.client.jobs
+
+import android.os.Build
+import android.provider.MediaStore
+import androidx.annotation.RequiresApi
+import androidx.work.Constraints
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
+import java.util.concurrent.TimeUnit
+
+internal class BackgroundJobManagerImpl(private val workManager: WorkManager) : BackgroundJobManager {
+
+    companion object {
+        const val TAG_CONTENT_SYNC = "content_sync"
+        const val MAX_CONTENT_TRIGGER_DELAY_MS = 1500L
+    }
+
+    @RequiresApi(Build.VERSION_CODES.N)
+    override fun scheduleContentObserverJob() {
+        val constrains = Constraints.Builder()
+            .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
+            .addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
+            .addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
+            .addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
+            .setTriggerContentMaxDelay(MAX_CONTENT_TRIGGER_DELAY_MS, TimeUnit.MILLISECONDS)
+            .build()
+
+        val request = OneTimeWorkRequest.Builder(ContentObserverWork::class.java)
+            .setConstraints(constrains)
+            .addTag(TAG_CONTENT_SYNC)
+            .build()
+
+        workManager.enqueue(request)
+    }
+}

+ 85 - 0
src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt

@@ -0,0 +1,85 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * 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.nextcloud.client.jobs
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.evernote.android.job.JobRequest
+import com.evernote.android.job.util.support.PersistableBundleCompat
+import com.nextcloud.client.device.PowerManagementService
+import com.owncloud.android.datamodel.SyncedFolderProvider
+import com.owncloud.android.jobs.FilesSyncJob
+import com.owncloud.android.jobs.MediaFoldersDetectionJob
+
+/**
+ * This work is triggered when OS detects change in media folders.
+ *
+ * It fires media detection job and sync job and finishes immediately.
+ *
+ * This job must not be started on API < 24.
+ */
+@RequiresApi(Build.VERSION_CODES.N)
+class ContentObserverWork(
+    appContext: Context,
+    private val params: WorkerParameters,
+    private val syncerFolderProvider: SyncedFolderProvider,
+    private val powerManagementService: PowerManagementService,
+    private val backgroundJobManager: BackgroundJobManager
+) : Worker(appContext, params) {
+
+    override fun doWork(): Result {
+        if (params.triggeredContentUris.size > 0) {
+            checkAndStartFileSyncJob()
+            startMediaFolderDetectionJob()
+        }
+        recheduleSelf()
+        return Result.success()
+    }
+
+    private fun recheduleSelf() {
+        backgroundJobManager.scheduleContentObserverJob()
+    }
+
+    private fun checkAndStartFileSyncJob() {
+        val syncFolders = syncerFolderProvider.countEnabledSyncedFolders() > 0
+        if (!powerManagementService.isPowerSavingEnabled && syncFolders) {
+            val persistableBundleCompat = PersistableBundleCompat()
+            persistableBundleCompat.putBoolean(FilesSyncJob.SKIP_CUSTOM, true)
+
+            JobRequest.Builder(FilesSyncJob.TAG)
+                .startNow()
+                .setExtras(persistableBundleCompat)
+                .setUpdateCurrent(false)
+                .build()
+                .schedule()
+        }
+    }
+
+    private fun startMediaFolderDetectionJob() {
+        JobRequest.Builder(MediaFoldersDetectionJob.TAG)
+            .startNow()
+            .setUpdateCurrent(false)
+            .build()
+            .schedule()
+    }
+}

+ 50 - 0
src/main/java/com/nextcloud/client/jobs/JobsModule.kt

@@ -0,0 +1,50 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * 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.nextcloud.client.jobs
+
+import android.content.Context
+import android.content.ContextWrapper
+import androidx.work.Configuration
+import androidx.work.WorkManager
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+@Module
+class JobsModule {
+
+    @Provides
+    @Singleton
+    fun backgroundJobManager(context: Context, factory: BackgroundJobFactory): BackgroundJobManager {
+        val configuration = Configuration.Builder()
+            .setWorkerFactory(factory)
+            .build()
+
+        val contextWrapper = object : ContextWrapper(context) {
+            override fun getApplicationContext(): Context {
+                return this
+            }
+        }
+
+        WorkManager.initialize(contextWrapper, configuration)
+        val wm = WorkManager.getInstance(context)
+        return BackgroundJobManagerImpl(wm)
+    }
+}

+ 1 - 1
src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.java

@@ -219,7 +219,7 @@ public class FirstRunActivity extends BaseActivity implements ViewPager.OnPageCh
 
             setAccount(account);
             userAccountManager.setCurrentOwnCloudAccount(account.name);
-            onAccountSet(false);
+            onAccountSet();
 
             Intent i = new Intent(this, FileDisplayActivity.class);
             i.setAction(FileDisplayActivity.RESTART);

+ 8 - 1
src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java

@@ -47,6 +47,7 @@ public final class AppPreferencesImpl implements AppPreferences {
      */
     public static final String AUTO_PREF__LAST_SEEN_VERSION_CODE = "lastSeenVersionCode";
     public static final String STORAGE_PATH = "storage_path";
+    public static final float DEFAULT_GRID_COLUMN = 4.0f;
     private static final String AUTO_PREF__LAST_UPLOAD_PATH = "last_upload_path";
     private static final String AUTO_PREF__UPLOAD_FROM_LOCAL_LAST_PATH = "upload_from_local_last_path";
     private static final String AUTO_PREF__UPLOAD_FILE_EXTENSION_MAP_URL = "prefs_upload_file_extension_map_url";
@@ -358,7 +359,13 @@ public final class AppPreferencesImpl implements AppPreferences {
      */
     @Override
     public float getGridColumns() {
-        return preferences.getFloat(AUTO_PREF__GRID_COLUMNS, 4.0f);
+        float columns = preferences.getFloat(AUTO_PREF__GRID_COLUMNS, DEFAULT_GRID_COLUMN);
+
+        if (columns < 0) {
+            return DEFAULT_GRID_COLUMN;
+        } else {
+            return columns;
+        }
     }
 
     /**

+ 20 - 4
src/main/java/com/owncloud/android/MainApp.java

@@ -49,6 +49,7 @@ import com.nextcloud.client.device.PowerManagementService;
 import com.nextcloud.client.di.ActivityInjector;
 import com.nextcloud.client.di.DaggerAppComponent;
 import com.nextcloud.client.errorhandling.ExceptionHandler;
+import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.logger.LegacyLoggerAdapter;
 import com.nextcloud.client.logger.Logger;
 import com.nextcloud.client.network.ConnectivityService;
@@ -157,6 +158,9 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
     @Inject
     AppInfo appInfo;
 
+    @Inject
+    BackgroundJobManager backgroundJobManager;
+
     private PassCodeManager passCodeManager;
 
     @SuppressWarnings("unused")
@@ -185,6 +189,14 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
         return powerManagementService;
     }
 
+    /**
+     * Temporary getter enabling intermediate refactoring.
+     * TODO: remove when FileSyncHelper is refactored/removed
+     */
+    public BackgroundJobManager getBackgroundJobManager() {
+        return backgroundJobManager;
+    }
+
     private String getAppProcessName() {
         String processName = "";
         if(Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
@@ -288,8 +300,11 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
                 Log_OC.d("Debug", "Failed to disable uri exposure");
             }
         }
-
-        initSyncOperations(uploadsStorageManager, accountManager, connectivityService, powerManagementService);
+        initSyncOperations(uploadsStorageManager,
+                           accountManager,
+                           connectivityService,
+                           powerManagementService,
+                           backgroundJobManager);
         initContactsBackup(accountManager);
         notificationChannels();
 
@@ -446,7 +461,8 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
         final UploadsStorageManager uploadsStorageManager,
         final UserAccountManager accountManager,
         final ConnectivityService connectivityService,
-        final PowerManagementService powerManagementService
+        final PowerManagementService powerManagementService,
+        final BackgroundJobManager jobManager
     ) {
         updateToAutoUpload();
         cleanOldEntries();
@@ -464,7 +480,7 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
 
         initiateExistingAutoUploadEntries();
 
-        FilesSyncHelper.scheduleFilesSyncIfNeeded(mContext);
+        FilesSyncHelper.scheduleFilesSyncIfNeeded(mContext, jobManager);
         FilesSyncHelper.restartJobsIfNeeded(
             uploadsStorageManager,
             accountManager,

+ 48 - 21
src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java

@@ -62,6 +62,7 @@ import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.preference.PreferenceManager;
+import android.provider.DocumentsContract;
 import android.text.Editable;
 import android.text.InputType;
 import android.text.TextUtils;
@@ -83,13 +84,14 @@ import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import android.widget.EditText;
 import android.widget.ImageButton;
+import android.widget.ImageView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 import android.widget.TextView.OnEditorActionListener;
+import android.widget.Toast;
 
 import com.blikoon.qrcodescanner.QrCodeActivity;
 import com.google.android.material.snackbar.Snackbar;
-import com.google.android.material.textfield.TextInputLayout;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.device.DeviceInfo;
 import com.nextcloud.client.di.Injectable;
@@ -119,7 +121,6 @@ import com.owncloud.android.operations.GetServerInfoOperation;
 import com.owncloud.android.services.OperationsService;
 import com.owncloud.android.services.OperationsService.OperationsServiceBinder;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
-import com.owncloud.android.ui.components.CustomEditText;
 import com.owncloud.android.ui.dialog.CredentialsDialogFragment;
 import com.owncloud.android.ui.dialog.IndeterminateProgressDialog;
 import com.owncloud.android.ui.dialog.SslUntrustedCertDialog;
@@ -189,15 +190,12 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
     private static final String KEY_USERNAME = "USERNAME";
     private static final String KEY_PASSWORD = "PASSWORD";
     private static final String KEY_ASYNC_TASK_IN_PROGRESS = "AUTH_IN_PROGRESS";
-    private static final String WEB_LOGIN = "/index.php/login/flow";
+    public static final String WEB_LOGIN = "/index.php/login/flow";
     public static final String PROTOCOL_SUFFIX = "://";
     public static final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":";
     public static final String HTTPS_PROTOCOL = "https://";
     public static final String HTTP_PROTOCOL = "http://";
 
-    public static final String REGULAR_SERVER_INPUT_TYPE = "regular";
-    public static final String SUBDOMAIN_SERVER_INPUT_TYPE = "prefix";
-    public static final String DIRECTORY_SERVER_INPUT_TYPE = "suffix";
     public static final int NO_ICON = 0;
     public static final String EMPTY_STRING = "";
 
@@ -216,9 +214,10 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
     private AccountManager mAccountMgr;
 
     /// Server PRE-Fragment elements
-    private CustomEditText mHostUrlInput;
+    private EditText mHostUrlInput;
     private View mRefreshButton;
     private TextView mServerStatusView;
+    private ImageView scanQR;
 
     private TextWatcher mHostUrlInputWatcher;
     private String mServerStatusText = EMPTY_STRING;
@@ -254,8 +253,6 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
     private boolean webViewLoginMethod;
     private String webViewUser;
     private String webViewPassword;
-    private TextInputLayout mUsernameInputLayout;
-    private TextInputLayout mPasswordInputLayout;
     private boolean forceOldLoginMethod;
 
     @Inject UserAccountManager accountManager;
@@ -422,8 +419,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
                     // initialize general UI elements
                     initOverallUi();
 
-                    mPasswordInputLayout.setVisibility(View.VISIBLE);
-                    mUsernameInputLayout.setVisibility(View.VISIBLE);
+                    mPasswordInput.setVisibility(View.VISIBLE);
+                    mUsernameInput.setVisibility(View.VISIBLE);
                     mUsernameInput.requestFocus();
                     mAuthStatusView.setVisibility(View.INVISIBLE);
                     mServerStatusView.setVisibility(View.INVISIBLE);
@@ -607,8 +604,6 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
      */
     private void initOverallUi() {
         mHostUrlInput = findViewById(R.id.hostUrlInput);
-        mUsernameInputLayout = findViewById(R.id.input_layout_account_username);
-        mPasswordInputLayout = findViewById(R.id.input_layout_account_password);
         mPasswordInput = findViewById(R.id.account_password);
         mUsernameInput = findViewById(R.id.account_username);
         mAuthStatusView = findViewById(R.id.auth_status_text);
@@ -618,7 +613,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         mOkButton = findViewById(R.id.buttonOK);
         mOkButton.setOnClickListener(v -> onOkClick());
 
-        ImageButton scanQR = findViewById(R.id.scanQR);
+        scanQR = findViewById(R.id.scanQR);
         if (deviceInfo.hasCamera(this)) {
             scanQR.setOnClickListener(v -> onScan());
         } else {
@@ -900,7 +895,14 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         Uri data = intent.getData();
 
         if (data != null && data.toString().startsWith(getString(R.string.login_data_own_scheme))) {
-            parseAndLoginFromWebView(data.toString());
+            if (!getResources().getBoolean(R.bool.multiaccount_support) &&
+                accountManager.getAccounts().length == 1) {
+                Toast.makeText(this, R.string.no_mutliple_accounts_allowed, Toast.LENGTH_LONG).show();
+                finish();
+                return;
+            } else {
+                parseAndLoginFromWebView(data.toString());
+            }
         }
 
         if (intent.getBooleanExtra(EXTRA_USE_PROVIDER_AS_WEBLOGIN, false)) {
@@ -928,7 +930,14 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
             String dataString = getIntent().getDataString();
             if (dataString != null) {
                 try {
-                    populateLoginFields(dataString);
+                    if (!getResources().getBoolean(R.bool.multiaccount_support) &&
+                        accountManager.getAccounts().length == 1) {
+                        Toast.makeText(this, R.string.no_mutliple_accounts_allowed, Toast.LENGTH_LONG).show();
+                        finish();
+                        return;
+                    } else {
+                        populateLoginFields(dataString);
+                    }
                 } catch (IllegalArgumentException e) {
                     DisplayUtils.showSnackMessage(findViewById(R.id.scroll), R.string.auth_illegal_login_used);
                     Log_OC.e(TAG, "Illegal login data URL used, no Login pre-fill!", e);
@@ -1344,8 +1353,14 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
 
     private void setOldLoginVisibility(int visible) {
         mOkButton.setVisibility(visible);
-        mUsernameInputLayout.setVisibility(visible);
-        mPasswordInputLayout.setVisibility(visible);
+        mUsernameInput.setVisibility(visible);
+        mPasswordInput.setVisibility(visible);
+
+        if (View.VISIBLE == visible) {
+            scanQR.setVisibility(View.GONE);
+        } else {
+            scanQR.setVisibility(View.VISIBLE);
+        }
     }
 
     private boolean authSupported(AuthenticationMethod authMethod) {
@@ -1563,7 +1578,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
 
                 initOverallUi();
 
-                CustomEditText serverAddressField = findViewById(R.id.hostUrlInput);
+                EditText serverAddressField = findViewById(R.id.hostUrlInput);
                 serverAddressField.setText(mServerInfo.mBaseUrl);
 
                 findViewById(R.id.server_status_text).setVisibility(View.GONE);
@@ -1616,7 +1631,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
 
                     initOverallUi();
 
-                    CustomEditText serverAddressField = findViewById(R.id.hostUrlInput);
+                    EditText serverAddressField = findViewById(R.id.hostUrlInput);
                     serverAddressField.setText(mServerInfo.mBaseUrl);
 
                     findViewById(R.id.server_status_text).setVisibility(View.GONE);
@@ -1756,6 +1771,13 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
             setAccountAuthenticatorResult(intent.getExtras());
             setResult(RESULT_OK, intent);
 
+            // notify Document Provider
+            if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+                String authority = getResources().getString(R.string.document_provider_authority);
+                Uri rootsUri = DocumentsContract.buildRootsUri(authority);
+                getContentResolver().notifyChange(rootsUri, null);
+            }
+
             return true;
         }
     }
@@ -2059,7 +2081,12 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
                 return;
             }
 
-            parseAndLoginFromWebView(result);
+            if (!getResources().getBoolean(R.bool.multiaccount_support) &&
+                accountManager.getAccounts().length == 1) {
+                Toast.makeText(this, R.string.no_mutliple_accounts_allowed, Toast.LENGTH_LONG).show();
+            } else {
+                parseAndLoginFromWebView(result);
+            }
         }
     }
 

+ 7 - 0
src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.java

@@ -3,6 +3,7 @@ package com.owncloud.android.authentication;
 import android.net.Uri;
 import android.os.Bundle;
 import android.widget.TextView;
+import android.widget.Toast;
 
 import com.owncloud.android.R;
 import com.owncloud.android.utils.ThemeUtils;
@@ -12,6 +13,12 @@ public class DeepLinkLoginActivity extends AuthenticatorActivity {
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
+        if (!getResources().getBoolean(R.bool.multiaccount_support) &&
+            accountManager.getAccounts().length == 1) {
+            Toast.makeText(this, R.string.no_mutliple_accounts_allowed, Toast.LENGTH_LONG).show();
+            return;
+        }
+
         setContentView(R.layout.deep_link_login);
 
         Uri data = getIntent().getData();

+ 7 - 1
src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -343,10 +343,16 @@ public class FileDataStorageManager {
             ContentValues cv = createContentValueForFile(file, folder);
 
             if (fileExists(file.getFileId()) || fileExists(file.getRemotePath())) {
+                long fileId;
+                if (file.getFileId() != -1) {
+                    fileId = file.getFileId();
+                } else {
+                    fileId = getFileByPath(file.getRemotePath()).getFileId();
+                }
                 // updating an existing file
                 operations.add(ContentProviderOperation.newUpdate(ProviderTableMeta.CONTENT_URI)
                         .withValues(cv)
-                        .withSelection(ProviderTableMeta._ID + "=?", new String[]{String.valueOf(file.getFileId())})
+                                   .withSelection(ProviderTableMeta._ID + "=?", new String[]{String.valueOf(fileId)})
                         .build());
             } else {
                 // adding a new file

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

@@ -119,7 +119,7 @@ public final class MediaProvider {
                                 MediaStore.MediaColumns.DATA));
 
                         // check if valid path and file exists
-                        if (filePath != null && filePath.lastIndexOf('/') > 0 && new File(filePath).exists()) {
+                        if (isValidAndExistingFilePath(filePath)) {
                             mediaFolder.filePaths.add(filePath);
                             mediaFolder.absolutePath = filePath.substring(0, filePath.lastIndexOf('/'));
                         }
@@ -127,7 +127,7 @@ public final class MediaProvider {
                     cursorImages.close();
 
                     // only do further work if folder is not within the Nextcloud app itself
-                    if (mediaFolder.absolutePath != null && !mediaFolder.absolutePath.startsWith(dataPath)) {
+                    if (isFolderOutsideOfAppPath(dataPath, mediaFolder)) {
 
                         // count images
                         Cursor count = contentResolver.query(
@@ -152,6 +152,14 @@ public final class MediaProvider {
         return mediaFolders;
     }
 
+    private static boolean isFolderOutsideOfAppPath(String dataPath, MediaFolder mediaFolder) {
+        return mediaFolder.absolutePath != null && !mediaFolder.absolutePath.startsWith(dataPath);
+    }
+
+    private static boolean isValidAndExistingFilePath(String filePath) {
+        return filePath != null && filePath.lastIndexOf('/') > 0 && new File(filePath).exists();
+    }
+
     private static void checkPermissions(@Nullable Activity activity) {
         if (activity != null &&
                 !PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
@@ -229,7 +237,7 @@ public final class MediaProvider {
                     cursorVideos.close();
 
                     // only do further work if folder is not within the Nextcloud app itself
-                    if (mediaFolder.absolutePath != null && !mediaFolder.absolutePath.startsWith(dataPath)) {
+                    if (isFolderOutsideOfAppPath(dataPath, mediaFolder)) {
 
                         // count images
                         Cursor count = contentResolver.query(

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

@@ -189,7 +189,7 @@ public class SyncedFolderProvider extends Observable {
         Cursor cursor = mContentResolver.query(
             ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
             null,
-            ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH + "LIKE ? AND " +
+            ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH + " LIKE ? AND " +
                 ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT + " =? ",
             new String[]{localPath + "%", account.name},
             null

+ 4 - 1
src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java

@@ -29,6 +29,7 @@ import android.content.Intent;
 
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.device.PowerManagementService;
+import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.network.ConnectivityService;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.datamodel.UploadsStorageManager;
@@ -51,6 +52,7 @@ public class BootupBroadcastReceiver extends BroadcastReceiver {
     @Inject UploadsStorageManager uploadsStorageManager;
     @Inject ConnectivityService connectivityService;
     @Inject PowerManagementService powerManagementService;
+    @Inject BackgroundJobManager backgroundJobManager;
 
     /**
      * Receives broadcast intent reporting that the system was just boot up.
@@ -66,7 +68,8 @@ public class BootupBroadcastReceiver extends BroadcastReceiver {
             MainApp.initSyncOperations(uploadsStorageManager,
                                        accountManager,
                                        connectivityService,
-                                       powerManagementService);
+                                       powerManagementService,
+                                       backgroundJobManager);
             MainApp.initContactsBackup(accountManager);
         } else {
             Log_OC.d(TAG, "Getting wrong intent: " + intent.getAction());

+ 96 - 65
src/main/java/com/owncloud/android/jobs/AccountRemovalJob.java

@@ -28,8 +28,10 @@ import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.AccountManagerCallback;
 import android.accounts.AccountManagerFuture;
-import android.content.ContentResolver;
 import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.DocumentsContract;
 import android.text.TextUtils;
 
 import com.evernote.android.job.Job;
@@ -60,6 +62,7 @@ import com.owncloud.android.utils.PushUtils;
 
 import org.greenrobot.eventbus.EventBus;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -73,7 +76,6 @@ import static com.owncloud.android.ui.activity.ManageAccountsActivity.PENDING_FO
 /**
  * Removes account and all local files
  */
-
 public class AccountRemovalJob extends Job implements AccountManagerCallback<Boolean> {
     public static final String TAG = "AccountRemovalJob";
     public static final String ACCOUNT = "account";
@@ -97,36 +99,17 @@ public class AccountRemovalJob extends Job implements AccountManagerCallback<Boo
         boolean remoteWipe = bundle.getBoolean(REMOTE_WIPE, false);
 
         if (account != null && accountManager != null) {
+            ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(context.getContentResolver());
+
             // disable contact backup job
             ContactsPreferenceActivity.cancelContactBackupJobForAccount(context, account);
 
-            OwnCloudClient client = null;
-            try {
-                OwnCloudAccount ocAccount = new OwnCloudAccount(account, MainApp.getAppContext());
-                client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount,
-                                                                                         MainApp.getAppContext());
-            } catch (Exception e) {
-                Log_OC.e(this, "Could not create client", e);
-            }
-
-            try {
-                AccountManagerFuture<Boolean> accountRemoval = accountManager.removeAccount(account, this, null);
-                boolean removal = accountRemoval.getResult();
-
-                if (!removal) {
-                    Log_OC.e(this, "Account removal of " + account.name + " failed!");
-                }
-            } catch (Exception e) {
-                Log_OC.e(this, "Account removal of " + account.name + " failed!", e);
-            }
+            removeAccount(account, accountManager);
 
             FileDataStorageManager storageManager = new FileDataStorageManager(account, context.getContentResolver());
 
-            File tempDir = new File(FileStorageUtils.getTemporalPath(account.name));
-            File saveDir = new File(FileStorageUtils.getSavePath(account.name));
-
-            FileStorageUtils.deleteRecursively(tempDir, storageManager);
-            FileStorageUtils.deleteRecursively(saveDir, storageManager);
+            // remove all files
+            removeFiles(account, storageManager);
 
             // delete all database entries
             storageManager.deleteAllFiles();
@@ -134,73 +117,121 @@ public class AccountRemovalJob extends Job implements AccountManagerCallback<Boo
             // remove contact backup job
             ContactsPreferenceActivity.cancelContactBackupJobForAccount(context, account);
 
-            ContentResolver contentResolver = context.getContentResolver();
-
             // disable daily backup
-            ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(contentResolver);
-
             arbitraryDataProvider.storeOrUpdateKeyValue(account.name,
                                                         ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP,
                                                         "false");
 
-            String arbitraryDataPushString;
-
-            if (!TextUtils.isEmpty(arbitraryDataPushString = arbitraryDataProvider.getValue(
-                account, PushUtils.KEY_PUSH)) &&
-                !TextUtils.isEmpty(context.getResources().getString(R.string.push_server_url))) {
-                Gson gson = new Gson();
-                PushConfigurationState pushArbitraryData = gson.fromJson(arbitraryDataPushString,
-                                                                         PushConfigurationState.class);
-                pushArbitraryData.setShouldBeDeleted(true);
-                arbitraryDataProvider.storeOrUpdateKeyValue(account.name, PushUtils.KEY_PUSH,
-                                                            gson.toJson(pushArbitraryData));
-
-                PushUtils.pushRegistrationToServer(userAccountManager, pushArbitraryData.getPushToken());
-            }
+            // unregister push notifications
+            unregisterPushNotifications(context, account, arbitraryDataProvider);
 
             // remove pending account removal
             arbitraryDataProvider.deleteKeyForAccount(account.name, PENDING_FOR_REMOVAL);
 
             // remove synced folders set for account
-            SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(context.getContentResolver(),
-                                                                                 AppPreferencesImpl.fromContext(context));
-            List<SyncedFolder> syncedFolders = syncedFolderProvider.getSyncedFolders();
-
-            List<Long> syncedFolderIds = new ArrayList<>();
-
-            for (SyncedFolder syncedFolder : syncedFolders) {
-                if (syncedFolder.getAccount().equals(account.name)) {
-                    arbitraryDataProvider.deleteKeyForAccount(FilesSyncHelper.GLOBAL,
-                                                              FilesSyncHelper.SYNCEDFOLDERINITIATED + syncedFolder.getId());
-                    syncedFolderIds.add(syncedFolder.getId());
-                }
-            }
-
-            syncedFolderProvider.deleteSyncFoldersForAccount(account);
+            remoceSyncedFolders(context, account, arbitraryDataProvider);
 
+            // delete all uploads for account
             uploadsStorageManager.removeAccountUploads(account);
 
-            FilesystemDataProvider filesystemDataProvider = new FilesystemDataProvider(context.getContentResolver());
-
-            for (long syncedFolderId : syncedFolderIds) {
-                filesystemDataProvider.deleteAllEntriesForSyncedFolder(Long.toString(syncedFolderId));
-            }
-
             // delete stored E2E keys
             arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.PRIVATE_KEY);
             arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.PUBLIC_KEY);
 
+            OwnCloudClient client = createClient(account);
             if (remoteWipe && client != null) {
                 String authToken = client.getCredentials().getAuthToken();
                 new RemoteWipeSuccessRemoteOperation(authToken).execute(client);
             }
 
+            // notify Document Provider
+            if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+                String authority = context.getResources().getString(R.string.document_provider_authority);
+                Uri rootsUri = DocumentsContract.buildRootsUri(authority);
+                context.getContentResolver().notifyChange(rootsUri, null);
+            }
+
             return Result.SUCCESS;
         } else {
             return Result.FAILURE;
         }
     }
 
+    private void unregisterPushNotifications(Context context, Account account, ArbitraryDataProvider arbitraryDataProvider) {
+        String arbitraryDataPushString;
+
+        if (!TextUtils.isEmpty(arbitraryDataPushString = arbitraryDataProvider.getValue(
+            account, PushUtils.KEY_PUSH)) &&
+            !TextUtils.isEmpty(context.getResources().getString(R.string.push_server_url))) {
+            Gson gson = new Gson();
+            PushConfigurationState pushArbitraryData = gson.fromJson(arbitraryDataPushString,
+                                                                     PushConfigurationState.class);
+            pushArbitraryData.setShouldBeDeleted(true);
+            arbitraryDataProvider.storeOrUpdateKeyValue(account.name, PushUtils.KEY_PUSH,
+                                                        gson.toJson(pushArbitraryData));
+
+            PushUtils.pushRegistrationToServer(userAccountManager, pushArbitraryData.getPushToken());
+        }
+    }
+
+    private void remoceSyncedFolders(Context context, Account account, ArbitraryDataProvider arbitraryDataProvider) {
+        SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(context.getContentResolver(),
+                                                                             AppPreferencesImpl.fromContext(context));
+        List<SyncedFolder> syncedFolders = syncedFolderProvider.getSyncedFolders();
+
+        List<Long> syncedFolderIds = new ArrayList<>();
+
+        for (SyncedFolder syncedFolder : syncedFolders) {
+            if (syncedFolder.getAccount().equals(account.name)) {
+                arbitraryDataProvider.deleteKeyForAccount(FilesSyncHelper.GLOBAL,
+                                                          FilesSyncHelper.SYNCEDFOLDERINITIATED + syncedFolder.getId());
+                syncedFolderIds.add(syncedFolder.getId());
+            }
+        }
+
+        syncedFolderProvider.deleteSyncFoldersForAccount(account);
+
+        FilesystemDataProvider filesystemDataProvider = new FilesystemDataProvider(context.getContentResolver());
+
+        for (long syncedFolderId : syncedFolderIds) {
+            filesystemDataProvider.deleteAllEntriesForSyncedFolder(Long.toString(syncedFolderId));
+        }
+    }
+
+    private void removeFiles(Account account, FileDataStorageManager storageManager) {
+        File tempDir = new File(FileStorageUtils.getTemporalPath(account.name));
+        File saveDir = new File(FileStorageUtils.getSavePath(account.name));
+
+        FileStorageUtils.deleteRecursively(tempDir, storageManager);
+        FileStorageUtils.deleteRecursively(saveDir, storageManager);
+    }
+
+    private void removeAccount(Account account, AccountManager accountManager) {
+        try {
+            AccountManagerFuture<Boolean> accountRemoval = accountManager.removeAccount(account, this, null);
+            boolean removal = accountRemoval.getResult();
+
+            if (!removal) {
+                Log_OC.e(this, "Account removal of " + account.name + " failed!");
+            }
+        } catch (Exception e) {
+            Log_OC.e(this, "Account removal of " + account.name + " failed!", e);
+        }
+    }
+
+    @Nullable
+    private OwnCloudClient createClient(Account account) {
+        OwnCloudClient client = null;
+        try {
+            OwnCloudAccount ocAccount = new OwnCloudAccount(account, MainApp.getAppContext());
+            client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount,
+                                                                                     MainApp.getAppContext());
+        } catch (Exception e) {
+            Log_OC.e(this, "Could not create client", e);
+        }
+        return client;
+    }
+
     @Override
     public void run(AccountManagerFuture<Boolean> future) {
         if (future.isDone()) {

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

@@ -64,8 +64,6 @@ import java.util.TimeZone;
 import androidx.annotation.NonNull;
 import androidx.exifinterface.media.ExifInterface;
 
-import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
-
 /*
     Job that:
         - restarts existing jobs if required
@@ -74,7 +72,7 @@ import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
  */
 public class FilesSyncJob extends Job {
     public static final String TAG = "FilesSyncJob";
-    static final String SKIP_CUSTOM = "skipCustom";
+    public static final String SKIP_CUSTOM = "skipCustom";
     public static final String OVERRIDE_POWER_SAVING = "overridePowerSaving";
     private static final String WAKELOCK_TAG_SEPARATION = ":";
 
@@ -198,25 +196,15 @@ public class FilesSyncJob extends Job {
                 remotePath = syncedFolder.getRemotePath();
             }
 
-            if (!subfolderByDate) {
-                String adaptedPath = file.getAbsolutePath()
-                        .replace(syncedFolder.getLocalPath(), "")
-                        .replace(PATH_SEPARATOR + file.getName(), "");
-                remotePath += adaptedPath;
-            }
-
-            String relativeSubfolderPath = new File(path.replace(syncedFolder.getLocalPath(), ""))
-                .getParentFile().getAbsolutePath();
-
             requester.uploadFileWithOverwrite(
                     context,
                     account,
                     file.getAbsolutePath(),
                     FileStorageUtils.getInstantUploadFilePath(
+                        file,
                         currentLocale,
                         remotePath,
-                        relativeSubfolderPath,
-                        file.getName(),
+                        syncedFolder.getLocalPath(),
                         lastModificationTime,
                         subfolderByDate),
                     uploadAction,

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

@@ -111,6 +111,19 @@ public class MediaFoldersDetectionJob extends Job {
         if (!TextUtils.isEmpty(arbitraryDataString)) {
             mediaFoldersModel = gson.fromJson(arbitraryDataString, MediaFoldersModel.class);
 
+            // merge new detected paths with already notified ones
+            for (String existingImageFolderPath : mediaFoldersModel.getImageMediaFolders()) {
+                if (!imageMediaFolderPaths.contains(existingImageFolderPath)) {
+                    imageMediaFolderPaths.add(existingImageFolderPath);
+                }
+            }
+
+            for (String existingVideoFolderPath : mediaFoldersModel.getVideoMediaFolders()) {
+                if (!videoMediaFolderPaths.contains(existingVideoFolderPath)) {
+                    videoMediaFolderPaths.add(existingVideoFolderPath);
+                }
+            }
+
             // Store updated values
             arbitraryDataProvider.storeOrUpdateKeyValue(ACCOUNT_NAME_GLOBAL, KEY_MEDIA_FOLDERS, gson.toJson(new
                 MediaFoldersModel(imageMediaFolderPaths, videoMediaFolderPaths)));

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

@@ -1,102 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Mario Danic
- * Copyright (C) 2017 Mario Danic
- * Copyright (C) 2017 Nextcloud
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.owncloud.android.jobs;
-
-import android.app.job.JobParameters;
-import android.app.job.JobService;
-import android.os.Build;
-
-import com.evernote.android.job.JobRequest;
-import com.evernote.android.job.util.support.PersistableBundleCompat;
-import com.nextcloud.client.device.PowerManagementService;
-import com.nextcloud.client.preferences.AppPreferences;
-import com.owncloud.android.MainApp;
-import com.owncloud.android.datamodel.SyncedFolderProvider;
-import com.owncloud.android.utils.FilesSyncHelper;
-
-import androidx.annotation.RequiresApi;
-
-/*
-    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 {
-
-    private PowerManagementService powerManagementService;
-    private AppPreferences preferences;
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-
-        // Temporary workaround for https://github.com/nextcloud/android/issues/4147
-        // TODO: this must be fixed properly
-        MainApp app = (MainApp) getApplication();
-        powerManagementService = app.getPowerManagementService();
-        preferences = app.getPreferences();
-    }
-
-    @Override
-    public boolean onStartJob(JobParameters params) {
-
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-            if (params.getJobId() == FilesSyncHelper.ContentSyncJobId && params.getTriggeredContentAuthorities()
-                    != null && params.getTriggeredContentUris() != null
-                    && params.getTriggeredContentUris().length > 0) {
-
-                checkAndStartFileSyncJob();
-
-                new JobRequest.Builder(MediaFoldersDetectionJob.TAG)
-                        .startNow()
-                        .setUpdateCurrent(false)
-                        .build()
-                        .schedule();
-
-            }
-
-            FilesSyncHelper.scheduleJobOnN();
-        }
-
-        return true;
-    }
-
-    private void checkAndStartFileSyncJob() {
-        if (!powerManagementService.isPowerSavingEnabled() &&
-                new SyncedFolderProvider(getContentResolver(), preferences).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;
-    }
-}

+ 39 - 39
src/main/java/com/owncloud/android/operations/DetectAuthenticationMethodOperation.java

@@ -1,5 +1,5 @@
 /**
- *   ownCloud Android client application
+ * ownCloud Android client application
  *
  *   @author David A. Velasco
  *   Copyright (C) 2015 ownCloud Inc.
@@ -46,78 +46,78 @@ import java.util.Locale;
  * {@link RemoteOperationResult#getData()} a value of {@link AuthenticationMethod}.
  */
 public class DetectAuthenticationMethodOperation extends RemoteOperation {
-    
+
     private static final String TAG = DetectAuthenticationMethodOperation.class.getSimpleName();
-    
+
     public enum AuthenticationMethod {
         UNKNOWN,
         NONE,
-        BASIC_HTTP_AUTH, 
+        BASIC_HTTP_AUTH,
         SAML_WEB_SSO,
         BEARER_TOKEN
     }
-    
+
     private Context mContext;
-    
+
     /**
      * Constructor
-     * 
-     * @param context       Android context of the caller.
+     *
+     * @param context Android context of the caller.
      */
     public DetectAuthenticationMethodOperation(Context context) {
         mContext = context;
     }
-    
+
 
     /**
      *  Performs the operation.
-     * 
+     *
      *  Triggers a check of existence on the root folder of the server, granting
      *  that the request is not authenticated.
-     *  
+     *
      *  Analyzes the result of check to find out what authentication method, if
      *  any, is requested by the server.
      */
-	@Override
-	protected RemoteOperationResult run(OwnCloudClient client) {
+    @Override
+    protected RemoteOperationResult run(OwnCloudClient client) {
         RemoteOperationResult result = null;
         AuthenticationMethod authMethod = AuthenticationMethod.UNKNOWN;
-        
+
         RemoteOperation operation = new ExistenceCheckRemoteOperation("", mContext, false);
         client.clearCredentials();
         client.setFollowRedirects(false);
-        
+
         // try to access the root folder, following redirections but not SAML SSO redirections
         result = operation.execute(client);
-        String redirectedLocation = result.getRedirectedLocation(); 
+        String redirectedLocation = result.getRedirectedLocation();
         while (!TextUtils.isEmpty(redirectedLocation) && !result.isIdPRedirection()) {
             client.setBaseUri(Uri.parse(result.getRedirectedLocation()));
             result = operation.execute(client);
             redirectedLocation = result.getRedirectedLocation();
-        } 
+        }
 
-        // analyze response  
-        if (result.getHttpCode() == HttpStatus.SC_UNAUTHORIZED) {
+        // analyze response
+        if (result.getHttpCode() == HttpStatus.SC_UNAUTHORIZED || result.getHttpCode() == HttpStatus.SC_FORBIDDEN) {
             ArrayList<String> authHeaders = result.getAuthenticateHeaders();
 
             for (String header : authHeaders) {
                 // currently we only support basic auth
-                if (header.toLowerCase(Locale.ROOT).startsWith("basic")) {
+                if (header.toLowerCase(Locale.ROOT).contains("basic")) {
                     authMethod = AuthenticationMethod.BASIC_HTTP_AUTH;
                     break;
                 }
             }
             // else - fall back to UNKNOWN
-                    
+
         } else if (result.isSuccess()) {
             authMethod = AuthenticationMethod.NONE;
-            
+
         } else if (result.isIdPRedirection()) {
             authMethod = AuthenticationMethod.SAML_WEB_SSO;
         }
         // else - fall back to UNKNOWN
         Log_OC.d(TAG, "Authentication method found: " + authenticationMethodToString(authMethod));
-        
+
         if (authMethod != AuthenticationMethod.UNKNOWN) {
             result = new RemoteOperationResult(true, result.getHttpCode(), result.getHttpPhrase(), null);
         }
@@ -125,22 +125,22 @@ public class DetectAuthenticationMethodOperation extends RemoteOperation {
         data.add(authMethod);
         result.setData(data);
         return result;  // same result instance, so that other errors
-                        // can be handled by the caller transparently
-	}
-	
-	private String authenticationMethodToString(AuthenticationMethod value) {
-	    switch (value){
-	    case NONE:
-	        return "NONE";
-	    case BASIC_HTTP_AUTH:
-	        return "BASIC_HTTP_AUTH";
-	    case BEARER_TOKEN:
-	        return "BEARER_TOKEN";
-	    case SAML_WEB_SSO:
-	        return "SAML_WEB_SSO";
-	    default:
-            return "UNKNOWN";
-	    }
+        // can be handled by the caller transparently
+    }
+
+    private String authenticationMethodToString(AuthenticationMethod value) {
+        switch (value) {
+            case NONE:
+                return "NONE";
+            case BASIC_HTTP_AUTH:
+                return "BASIC_HTTP_AUTH";
+            case BEARER_TOKEN:
+                return "BEARER_TOKEN";
+            case SAML_WEB_SSO:
+                return "SAML_WEB_SSO";
+            default:
+                return "UNKNOWN";
+        }
     }
 
 }

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

@@ -416,6 +416,10 @@ public class RefreshFolderOperation extends RemoteOperation {
             // retrieve local data for the read file
             localFile = localFilesMap.remove(remoteFile.getRemotePath());
 
+            if (localFile == null) {
+                localFile = mStorageManager.getFileByPath(updatedFile.getRemotePath());
+            }
+
             // add to updatedFile data about LOCAL STATE (not existing in server)
             updatedFile.setLastSyncDateForProperties(mCurrentSyncTime);
 

+ 368 - 214
src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java

@@ -34,17 +34,19 @@ import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.graphics.Point;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract;
 import android.provider.DocumentsProvider;
 import android.util.Log;
+import android.util.SparseArray;
 import android.widget.Toast;
 
-import com.evernote.android.job.JobRequest;
-import com.evernote.android.job.util.Device;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.account.UserAccountManagerImpl;
 import com.nextcloud.client.preferences.AppPreferences;
@@ -55,12 +57,12 @@ import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.files.services.FileDownloader;
-import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation;
 import com.owncloud.android.operations.CopyFileOperation;
 import com.owncloud.android.operations.CreateFolderOperation;
 import com.owncloud.android.operations.MoveFileOperation;
@@ -68,7 +70,6 @@ import com.owncloud.android.operations.RefreshFolderOperation;
 import com.owncloud.android.operations.RemoveFileOperation;
 import com.owncloud.android.operations.RenameFileOperation;
 import com.owncloud.android.operations.SynchronizeFileOperation;
-import com.owncloud.android.operations.UploadFileOperation;
 import com.owncloud.android.ui.activity.ConflictsResolveActivity;
 import com.owncloud.android.ui.activity.SettingsActivity;
 import com.owncloud.android.utils.FileStorageUtils;
@@ -81,9 +82,11 @@ import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 
 import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
 import static com.owncloud.android.datamodel.OCFile.ROOT_PATH;
@@ -91,16 +94,25 @@ import static com.owncloud.android.datamodel.OCFile.ROOT_PATH;
 @TargetApi(Build.VERSION_CODES.KITKAT)
 public class DocumentsStorageProvider extends DocumentsProvider {
 
-    private static final String TAG = "DocumentsStorageProvider";
+    private static final String TAG = DocumentsStorageProvider.class.getSimpleName();
 
-    private FileDataStorageManager currentStorageManager;
-    private Map<Long, FileDataStorageManager> rootIdToStorageManager;
-    private OwnCloudClient client;
+    private static final long CACHE_EXPIRATION = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES);
 
     UserAccountManager accountManager;
 
+    private static final String DOCUMENTID_SEPARATOR = "/";
+    private static final int DOCUMENTID_PARTS = 2;
+    private final SparseArray<FileDataStorageManager> rootIdToStorageManager = new SparseArray<>();
+
+    private final Executor executor = Executors.newCachedThreadPool();
+
     @Override
-    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
+    public Cursor queryRoots(String[] projection) {
+
+        // always recreate storage manager collection, as it will change after account creation/removal
+        // and we need to serve document(tree)s with persist permissions
+        initiateStorageMap();
+
         Context context = MainApp.getAppContext();
         AppPreferences preferences = AppPreferencesImpl.fromContext(context);
         if (SettingsActivity.LOCK_PASSCODE.equals(preferences.getLockPreference()) ||
@@ -108,12 +120,9 @@ public class DocumentsStorageProvider extends DocumentsProvider {
             return new FileCursor();
         }
 
-        initiateStorageMap();
-
         final RootCursor result = new RootCursor(projection);
-
-        for (Account account : accountManager.getAccounts()) {
-            result.addRoot(account, getContext());
+        for(int i = 0; i < rootIdToStorageManager.size(); i++) {
+            result.addRoot(new Document(rootIdToStorageManager.valueAt(i), ROOT_PATH), getContext());
         }
 
         return result;
@@ -121,28 +130,12 @@ public class DocumentsStorageProvider extends DocumentsProvider {
 
     @Override
     public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
-        final long docId = Long.parseLong(documentId);
-        updateCurrentStorageManagerIfNeeded(docId);
-
-        if (currentStorageManager == null) {
+        Log.d(TAG, "queryDocument(), id=" + documentId);
 
-            for (Map.Entry<Long, FileDataStorageManager> entry : rootIdToStorageManager.entrySet()) {
-                if (entry.getValue().getFileById(docId) != null) {
-                    currentStorageManager = entry.getValue();
-                    break;
-                }
-            }
-        }
-
-        if (currentStorageManager == null) {
-            throw new FileNotFoundException("File with id " + documentId + " not found");
-        }
+        Document document = toDocument(documentId);
 
         final FileCursor result = new FileCursor(projection);
-        OCFile file = currentStorageManager.getFileById(docId);
-        if (file != null) {
-            result.addFile(file);
-        }
+        result.addFile(document);
 
         return result;
     }
@@ -151,34 +144,37 @@ public class DocumentsStorageProvider extends DocumentsProvider {
     @Override
     public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
         throws FileNotFoundException {
-
-        final long folderId = Long.parseLong(parentDocumentId);
-        updateCurrentStorageManagerIfNeeded(folderId);
+        Log.d(TAG, "queryChildDocuments(), id=" + parentDocumentId);
 
         Context context = getContext();
-
         if (context == null) {
             throw new FileNotFoundException("Context may not be null");
         }
 
-        Account account = currentStorageManager.getAccount();
-        final OCFile browsedDir = currentStorageManager.getFileById(folderId);
-        if (Device.getNetworkType(context).equals(JobRequest.NetworkType.UNMETERED)) {
-            RemoteOperationResult result = new RefreshFolderOperation(browsedDir, System.currentTimeMillis(), false,
-                                                                      false, true, currentStorageManager, account,
-                                                                      getContext()).execute(client);
+        Document parentFolder = toDocument(parentDocumentId);
 
-            if (!result.isSuccess()) {
-                throw new FileNotFoundException("Failed to update document " + parentDocumentId);
-            }
-        }
+        FileDataStorageManager storageManager = parentFolder.getStorageManager();
 
         final FileCursor resultCursor = new FileCursor(projection);
 
-        for (OCFile file : currentStorageManager.getFolderContent(browsedDir, false)) {
-            resultCursor.addFile(file);
+        for (OCFile file : storageManager.getFolderContent(parentFolder.getFile(), false)) {
+            resultCursor.addFile(new Document(storageManager, file));
         }
 
+        boolean isLoading = false;
+        if (parentFolder.isExpired()) {
+            final ReloadFolderDocumentTask task = new ReloadFolderDocumentTask(parentFolder, result -> {
+                getContext().getContentResolver().notifyChange(toNotifyUri(parentFolder), null, false);
+            });
+            task.executeOnExecutor(executor);
+            resultCursor.setLoadingTask(task);
+            isLoading = true;
+        }
+
+        final Bundle extra = new Bundle();
+        extra.putBoolean(DocumentsContract.EXTRA_LOADING, isLoading);
+        resultCursor.setExtras(extra);
+        resultCursor.setNotificationUri(getContext().getContentResolver(), toNotifyUri(parentFolder));
         return resultCursor;
     }
 
@@ -186,22 +182,18 @@ public class DocumentsStorageProvider extends DocumentsProvider {
     @Override
     public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal cancellationSignal)
             throws FileNotFoundException {
-        final long docId = Long.parseLong(documentId);
-        updateCurrentStorageManagerIfNeeded(docId);
+        Log.d(TAG, "openDocument(), id=" + documentId);
 
-        OCFile ocFile = currentStorageManager.getFileById(docId);
-
-        if (ocFile == null) {
-            throw new FileNotFoundException("File not found: " + documentId);
-        }
+        Document document = toDocument(documentId);
 
         Context context = getContext();
-
         if (context == null) {
             throw new FileNotFoundException("Context may not be null!");
         }
 
-        Account account = currentStorageManager.getAccount();
+        OCFile ocFile = document.getFile();
+        Account account = document.getAccount();
+
         if (!ocFile.isDown()) {
             Intent i = new Intent(getContext(), FileDownloader.class);
             i.putExtra(FileDownloader.EXTRA_ACCOUNT, account);
@@ -216,7 +208,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
                 if (!waitOrGetCancelled(cancellationSignal)) {
                     throw new FileNotFoundException("File with id " + documentId + " not found!");
                 }
-                ocFile = currentStorageManager.getFileById(docId);
+                ocFile = document.getFile();
 
                 if (ocFile == null) {
                     throw new FileNotFoundException("File with id " + documentId + " not found!");
@@ -226,11 +218,10 @@ public class DocumentsStorageProvider extends DocumentsProvider {
             OCFile finalFile = ocFile;
             Thread syncThread = new Thread(() -> {
                 try {
-                    FileDataStorageManager storageManager =
-                            new FileDataStorageManager(account, context.getContentResolver());
-                    SynchronizeFileOperation sfo =
-                            new SynchronizeFileOperation(finalFile, null, account, true, context);
-                    RemoteOperationResult result = sfo.execute(storageManager, context);
+                    FileDataStorageManager storageManager = new FileDataStorageManager(account, context.getContentResolver());
+                    RemoteOperationResult result = new SynchronizeFileOperation(finalFile, null, account,
+                                                                                true, context)
+                        .execute(storageManager, context);
                     if (result.getCode() == RemoteOperationResult.ResultCode.SYNC_CONFLICT) {
                         // ISSUE 5: if the user is not running the app (this is a service!),
                         // this can be very intrusive; a notification should be preferred
@@ -271,7 +262,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
                 return ParcelFileDescriptor.open(file, accessMode, handler, l -> {
                     RemoteOperationResult result = new SynchronizeFileOperation(newFile, oldFile, account, true,
                                                                                 context)
-                        .execute(client, currentStorageManager);
+                        .execute(document.getClient(), document.getStorageManager());
 
                     boolean success = result.isSuccess();
 
@@ -297,6 +288,11 @@ public class DocumentsStorageProvider extends DocumentsProvider {
     @Override
     public boolean onCreate() {
         accountManager = UserAccountManagerImpl.fromContext(getContext());
+
+        // initiate storage manager collection, because we need to serve document(tree)s
+        // with persist permissions
+        initiateStorageMap();
+
         return true;
     }
 
@@ -305,141 +301,140 @@ public class DocumentsStorageProvider extends DocumentsProvider {
                                                      Point sizeHint,
                                                      CancellationSignal signal)
             throws FileNotFoundException {
-        long docId = Long.parseLong(documentId);
-        updateCurrentStorageManagerIfNeeded(docId);
-
-        OCFile file = currentStorageManager.getFileById(docId);
-
-        if (file == null) {
-            throw new FileNotFoundException("File with id " + documentId + " not found!");
-        }
+        Log.d(TAG, "openDocumentThumbnail(), id=" + documentId);
 
         Context context = getContext();
-
         if (context == null) {
             throw new FileNotFoundException("Context may not be null!");
         }
 
+        Document document = toDocument(documentId);
+        
         boolean exists = ThumbnailsCacheManager.containsBitmap(ThumbnailsCacheManager.PREFIX_THUMBNAIL
-                                                                   + file.getRemoteId());
+                                                                   + document.getFile().getRemoteId());
 
         if (!exists) {
-            ThumbnailsCacheManager.generateThumbnailFromOCFile(file);
+            ThumbnailsCacheManager.generateThumbnailFromOCFile(document.getFile());
         }
 
         Uri uri = Uri.parse(UriUtils.URI_CONTENT_SCHEME + context.getResources().getString(
-            R.string.image_cache_provider_authority) + file.getRemotePath());
+            R.string.image_cache_provider_authority) + document.getRemotePath());
+        Log.d(TAG, "open thumbnail, uri=" + uri);
         return context.getContentResolver().openAssetFileDescriptor(uri, "r");
     }
 
     @Override
     public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
-        long docId = Long.parseLong(documentId);
-        updateCurrentStorageManagerIfNeeded(docId);
-
-        OCFile file = currentStorageManager.getFileById(docId);
+        Log.d(TAG, "renameDocument(), id=" + documentId);
 
-        if (file == null) {
-            throw new FileNotFoundException("File " + documentId + " not found!");
+        Context context = getContext();
+        if (context == null) {
+            throw new FileNotFoundException("Context may not be null!");
         }
 
-        RemoteOperationResult result = new RenameFileOperation(file.getRemotePath(), displayName)
-            .execute(client, currentStorageManager);
+        Document document = toDocument(documentId);
+
+        RemoteOperationResult result = new RenameFileOperation(document.getRemotePath(), displayName)
+            .execute(document.getClient(), document.getStorageManager());
 
         if (!result.isSuccess()) {
             throw new FileNotFoundException("Failed to rename document with documentId " + documentId + ": " +
                                                 result.getException());
         }
 
+        context.getContentResolver().notifyChange(toNotifyUri(document.getParent()), null, false);
+
         return null;
     }
 
     @Override
     public String copyDocument(String sourceDocumentId, String targetParentDocumentId) throws FileNotFoundException {
-        long sourceId = Long.parseLong(sourceDocumentId);
+        Log.d(TAG, "copyDocument(), id=" + sourceDocumentId);
 
-        updateCurrentStorageManagerIfNeeded(sourceId);
-
-        OCFile file = currentStorageManager.getFileById(sourceId);
-        if (file == null) {
-            throw new FileNotFoundException("File " + sourceDocumentId + " not found!");
+        Context context = getContext();
+        if (context == null) {
+            throw new FileNotFoundException("Context may not be null!");
         }
 
-        long targetId = Long.parseLong(targetParentDocumentId);
-        OCFile targetFolder = currentStorageManager.getFileById(targetId);
-        if (targetFolder == null) {
-            throw new FileNotFoundException("File " + targetParentDocumentId + " not found!");
-        }
+        Document document = toDocument(sourceDocumentId);
 
-        RemoteOperationResult result = new CopyFileOperation(file.getRemotePath(), targetFolder.getRemotePath())
-            .execute(client, currentStorageManager);
+        FileDataStorageManager storageManager = document.getStorageManager();
+        Document targetFolder = toDocument(targetParentDocumentId);
+
+        RemoteOperationResult result = new CopyFileOperation(document.getRemotePath(), targetFolder.getRemotePath())
+            .execute(document.getClient(), storageManager);
 
         if (!result.isSuccess()) {
             throw new FileNotFoundException("Failed to copy document with documentId " + sourceDocumentId
                                                 + " to " + targetParentDocumentId);
         }
 
-        Account account = currentStorageManager.getAccount();
+        Account account = document.getAccount();
 
-        RemoteOperationResult updateParent = new RefreshFolderOperation(targetFolder, System.currentTimeMillis(),
-                                                                        false, false, true, currentStorageManager,
-                                                                        account, getContext()).execute(client);
+        RemoteOperationResult updateParent = new RefreshFolderOperation(targetFolder.getFile(), System.currentTimeMillis(),
+                                                                        false, false, true, storageManager,
+                                                                        account, context)
+            .execute(targetFolder.getClient());
 
         if (!updateParent.isSuccess()) {
             throw new FileNotFoundException("Failed to copy document with documentId " + sourceDocumentId
                                                 + " to " + targetParentDocumentId);
         }
 
-        String newPath = targetFolder.getRemotePath() + file.getFileName();
+        String newPath = targetFolder.getRemotePath() + document.getFile().getFileName();
 
-        if (file.isFolder()) {
+        if (document.getFile().isFolder()) {
             newPath = newPath + PATH_SEPARATOR;
         }
-        OCFile newFile = currentStorageManager.getFileByPath(newPath);
+        Document newFile = new Document(storageManager, newPath);
+
+        context.getContentResolver().notifyChange(toNotifyUri(targetFolder), null, false);
 
-        return String.valueOf(newFile.getFileId());
+        return newFile.getDocumentId();
     }
 
     @Override
     public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId)
         throws FileNotFoundException {
-        long sourceId = Long.parseLong(sourceDocumentId);
-
-        updateCurrentStorageManagerIfNeeded(sourceId);
-
-        OCFile file = currentStorageManager.getFileById(sourceId);
+        Log.d(TAG, "moveDocument(), id=" + sourceDocumentId);
 
-        if (file == null) {
-            throw new FileNotFoundException("File " + sourceDocumentId + " not found!");
+        Context context = getContext();
+        if (context == null) {
+            throw new FileNotFoundException("Context may not be null!");
         }
 
-        long targetId = Long.parseLong(targetParentDocumentId);
-        OCFile targetFolder = currentStorageManager.getFileById(targetId);
-
-        if (targetFolder == null) {
-            throw new FileNotFoundException("File " + targetParentDocumentId + " not found!");
-        }
+        Document document = toDocument(sourceDocumentId);
+        Document targetFolder = toDocument(targetParentDocumentId);
 
-        RemoteOperationResult result = new MoveFileOperation(file.getRemotePath(), targetFolder.getRemotePath())
-            .execute(client, currentStorageManager);
+        RemoteOperationResult result = new MoveFileOperation(document.getRemotePath(), targetFolder.getRemotePath())
+            .execute(document.getClient(), document.getStorageManager());
 
         if (!result.isSuccess()) {
             throw new FileNotFoundException("Failed to move document with documentId " + sourceDocumentId
                                                 + " to " + targetParentDocumentId);
         }
 
-        return String.valueOf(file.getFileId());
+        Document sourceFolder = toDocument(sourceParentDocumentId);
+
+        getContext().getContentResolver().notifyChange(toNotifyUri(sourceFolder), null, false);
+        getContext().getContentResolver().notifyChange(toNotifyUri(targetFolder), null, false);
+
+        return sourceDocumentId;
     }
 
     @Override
     public Cursor querySearchDocuments(String rootId, String query, String[] projection) {
-        updateCurrentStorageManagerIfNeeded(rootId);
+        Log.d(TAG, "querySearchDocuments(), rootId=" + rootId);
 
-        OCFile root = currentStorageManager.getFileByPath(ROOT_PATH);
         FileCursor result = new FileCursor(projection);
 
-        for (OCFile f : findFiles(root, query)) {
-            result.addFile(f);
+        FileDataStorageManager storageManager = getStorageManager(rootId);
+        if (storageManager == null) {
+            return result;
+        }
+
+        for (Document d : findFiles(new Document(storageManager, ROOT_PATH), query)) {
+            result.addFile(d);
         }
 
         return result;
@@ -447,53 +442,73 @@ public class DocumentsStorageProvider extends DocumentsProvider {
 
     @Override
     public String createDocument(String documentId, String mimeType, String displayName) throws FileNotFoundException {
-        long docId = Long.parseLong(documentId);
-        updateCurrentStorageManagerIfNeeded(docId);
-
-        OCFile parent = currentStorageManager.getFileById(docId);
+        Log.d(TAG, "createDocument(), id=" + documentId);
 
-        if (parent == null) {
-            throw new FileNotFoundException("Parent file not found");
-        }
+        Document folderDocument = toDocument(documentId);
 
-        if ("vnd.android.document/directory".equalsIgnoreCase(mimeType)) {
-            return createFolder(parent, displayName, documentId);
+        if (DocumentsContract.Document.MIME_TYPE_DIR.equalsIgnoreCase(mimeType)) {
+            return createFolder(folderDocument, displayName);
         } else {
-            return createFile(parent, displayName, documentId);
+            return createFile(folderDocument, displayName);
         }
     }
 
-    private String createFolder(OCFile parent, String displayName, String documentId) throws FileNotFoundException {
+    private String createFolder(Document targetFolder, String displayName) throws FileNotFoundException {
+
+        Context context = getContext();
+
+        if (context == null) {
+            throw new FileNotFoundException("Context may not be null!");
+        }
 
-        CreateFolderOperation createFolderOperation = new CreateFolderOperation(parent.getRemotePath() + displayName
-                                                                                    + PATH_SEPARATOR, true);
-        RemoteOperationResult result = createFolderOperation.execute(client, currentStorageManager);
+        String newDirPath = targetFolder.getRemotePath() + displayName + PATH_SEPARATOR;
+        FileDataStorageManager storageManager = targetFolder.getStorageManager();
 
+        RemoteOperationResult result = new CreateFolderOperation(newDirPath, true)
+            .execute(targetFolder.getClient(), storageManager);
 
         if (!result.isSuccess()) {
             throw new FileNotFoundException("Failed to create document with name " +
-                                                displayName + " and documentId " + documentId);
+                                                displayName + " and documentId " + targetFolder.getDocumentId());
+        }
+
+        RemoteOperationResult updateParent = new RefreshFolderOperation(targetFolder.getFile(), System.currentTimeMillis(),
+                                                                        false, false, true, storageManager,
+                                                                        targetFolder.getAccount(), context)
+            .execute(targetFolder.getClient());
+
+        if (!updateParent.isSuccess()) {
+            throw new FileNotFoundException("Failed to create document with documentId " + targetFolder.getDocumentId());
         }
 
+        Document newFolder = new Document(storageManager, newDirPath);
 
-        String newDirPath = parent.getRemotePath() + displayName + PATH_SEPARATOR;
-        OCFile newFolder = currentStorageManager.getFileByPath(newDirPath);
+        context.getContentResolver().notifyChange(toNotifyUri(targetFolder), null, false);
 
-        return String.valueOf(newFolder.getFileId());
+        return newFolder.getDocumentId();
     }
 
-    private String createFile(OCFile parent, String displayName, String documentId) throws FileNotFoundException {
+    private String createFile(Document targetFolder, String displayName) throws FileNotFoundException {
         Context context = getContext();
-
         if (context == null) {
             throw new FileNotFoundException("Context may not be null!");
         }
 
-        Account account = currentStorageManager.getAccount();
+        Account account = targetFolder.getAccount();
 
         // create dummy file
         File tempDir = new File(FileStorageUtils.getTemporalPath(account.name));
+
+        if (!tempDir.exists() && !tempDir.mkdirs()) {
+            throw new FileNotFoundException("Temp folder could not be created: " + tempDir.getAbsolutePath());
+        }
+
         File emptyFile = new File(tempDir, displayName);
+
+        if (emptyFile.exists() && !emptyFile.delete()) {
+            throw new FileNotFoundException("Previous file could not be deleted");
+        }
+
         try {
             if (!emptyFile.createNewFile()) {
                 throw new FileNotFoundException("File could not be created");
@@ -502,30 +517,40 @@ public class DocumentsStorageProvider extends DocumentsProvider {
             throw new FileNotFoundException("File could not be created");
         }
 
-        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-        requester.uploadNewFile(getContext(), account, new String[]{emptyFile.getAbsolutePath()},
-                                new String[]{parent.getRemotePath() + displayName}, null,
-                                FileUploader.LOCAL_BEHAVIOUR_MOVE, true, UploadFileOperation.CREATED_BY_USER, false,
-                                false);
+        String newFilePath = targetFolder.getRemotePath() + displayName;
 
-        try {
-            Thread.sleep(2000);
-        } catch (InterruptedException e) {
-            Log_OC.e(TAG, "Thread interruption error");
+        // perform the upload, no need for chunked operation as we have a empty file
+        OwnCloudClient client = targetFolder.getClient();
+        RemoteOperationResult result = new UploadFileRemoteOperation(emptyFile.getAbsolutePath(),
+                                                                     newFilePath,
+                                                                     null,
+                                                                     "",
+                                                                     String.valueOf(System.currentTimeMillis() / 1000))
+            .execute(client);
+
+        if (!result.isSuccess()) {
+            throw new FileNotFoundException("Failed to upload document with path " + newFilePath);
         }
 
-        RemoteOperationResult updateParent = new RefreshFolderOperation(parent, System.currentTimeMillis(),
-                                                                        false, false, true, currentStorageManager,
-                                                                        account, getContext()).execute(client);
+        RemoteOperationResult updateParent = new RefreshFolderOperation(targetFolder.getFile(),
+                                                                        System.currentTimeMillis(),
+                                                                        false,
+                                                                        false,
+                                                                        true,
+                                                                        targetFolder.getStorageManager(),
+                                                                        account,
+                                                                        context)
+            .execute(client);
 
         if (!updateParent.isSuccess()) {
-            throw new FileNotFoundException("Failed to create document with documentId " + documentId);
+            throw new FileNotFoundException("Failed to create document with documentId " + targetFolder.getDocumentId());
         }
 
-        String newFilePath = parent.getRemotePath() + displayName;
-        OCFile newFile = currentStorageManager.getFileByPath(newFilePath);
+        Document newFile = new Document(targetFolder.getStorageManager(), newFilePath);
+
+        context.getContentResolver().notifyChange(toNotifyUri(targetFolder), null, false);
 
-        return String.valueOf(newFile.getFileId());
+        return newFile.getDocumentId();
     }
 
     @Override
@@ -535,76 +560,84 @@ public class DocumentsStorageProvider extends DocumentsProvider {
 
     @Override
     public void deleteDocument(String documentId) throws FileNotFoundException {
-        long docId = Long.parseLong(documentId);
-        updateCurrentStorageManagerIfNeeded(docId);
+        Log.d(TAG, "deleteDocument(), id=" + documentId);
 
-        OCFile file = currentStorageManager.getFileById(docId);
-
-        if (file == null) {
-            throw new FileNotFoundException("File " + documentId + " not found!");
+        Context context = getContext();
+        if (context == null) {
+            throw new FileNotFoundException("Context may not be null!");
         }
-        Account account = currentStorageManager.getAccount();
 
-        RemoveFileOperation removeFileOperation = new RemoveFileOperation(file.getRemotePath(), false, account, true,
-                                                                          getContext());
+        Document document = toDocument(documentId);
+
+        recursiveRevokePermission(document);
 
-        RemoteOperationResult result = removeFileOperation.execute(client, currentStorageManager);
+        RemoteOperationResult result = new RemoveFileOperation(document.getRemotePath(), false,
+                                                               document.getAccount(), true, context)
+            .execute(document.getClient(), document.getStorageManager());
 
         if (!result.isSuccess()) {
             throw new FileNotFoundException("Failed to delete document with documentId " + documentId);
         }
+
+        Document parentFolder = document.getParent();
+        context.getContentResolver().notifyChange(toNotifyUri(parentFolder), null, false);
     }
 
-    @SuppressLint("LongLogTag")
-    private void updateCurrentStorageManagerIfNeeded(long docId) {
-        if (rootIdToStorageManager == null) {
-            try {
-                queryRoots(FileCursor.DEFAULT_DOCUMENT_PROJECTION);
-            } catch (FileNotFoundException e) {
-                Log.e(TAG, "Failed to query roots");
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    private void recursiveRevokePermission(Document document) {
+        FileDataStorageManager storageManager = document.getStorageManager();
+        OCFile file = document.getFile();
+        if (file.isFolder()) {
+            for (OCFile child : storageManager.getFolderContent(file, false)) {
+                recursiveRevokePermission(new Document(storageManager, child));
             }
         }
 
-        if (currentStorageManager == null ||
-            rootIdToStorageManager.containsKey(docId) && currentStorageManager != rootIdToStorageManager.get(docId)) {
-            currentStorageManager = rootIdToStorageManager.get(docId);
-        }
+        revokeDocumentPermission(document.getDocumentId());
+    }
+
+    @Override
+    public boolean isChildDocument(String parentDocumentId, String documentId) {
+        Log.d(TAG, "isChildDocument(), parent=" + parentDocumentId + ", id=" + documentId);
 
         try {
-            Account account = currentStorageManager.getAccount();
-            OwnCloudAccount ocAccount = new OwnCloudAccount(account, MainApp.getAppContext());
-            client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, getContext());
-        } catch (OperationCanceledException | IOException | AuthenticatorException |
-            com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) {
-            Log_OC.e(TAG, "Failed to set client", e);
+            Document currentDocument = toDocument(documentId);
+            Document parentDocument = toDocument(parentDocumentId);
+
+            while (!ROOT_PATH.equals(currentDocument.getRemotePath())) {
+                currentDocument = currentDocument.getParent();
+                if (parentDocument.getFile().equals(currentDocument.getFile())) {
+                    return true;
+                }
+            }
+
+        } catch (FileNotFoundException e) {
+            Log.e(TAG, "failed to check for child document", e);
         }
+
+        return false;
     }
 
-    private void updateCurrentStorageManagerIfNeeded(String rootId) {
-        for (FileDataStorageManager data : rootIdToStorageManager.values()) {
-            if (data.getAccount().name.equals(rootId)) {
-                currentStorageManager = data;
+    private FileDataStorageManager getStorageManager(String rootId) {
+        for(int i = 0; i < rootIdToStorageManager.size(); i++) {
+            FileDataStorageManager storageManager = rootIdToStorageManager.valueAt(i);
+            if (storageManager.getAccount().name.equals(rootId)) {
+                return storageManager;
             }
         }
-    }
 
-    @SuppressLint("UseSparseArrays")
-    private void initiateStorageMap() throws FileNotFoundException {
+        return null;
+    }
 
-        Context context = getContext();
+    private void initiateStorageMap() {
 
-        final Account[] allAccounts = accountManager.getAccounts();
-        rootIdToStorageManager = new HashMap<>(allAccounts.length);
+        rootIdToStorageManager.clear();
 
-        if (context == null) {
-            throw new FileNotFoundException("Context may not be null!");
-        }
+        ContentResolver contentResolver = getContext().getContentResolver();
 
-        final ContentResolver contentResolver = context.getContentResolver();
-        for (Account account : allAccounts) {
+        for (Account account : accountManager.getAccounts()) {
             final FileDataStorageManager storageManager = new FileDataStorageManager(account, contentResolver);
-            final OCFile rootDir = storageManager.getFileByPath(ROOT_PATH);
-            rootIdToStorageManager.put(rootDir.getFileId(), storageManager);
+            rootIdToStorageManager.put(account.hashCode(), storageManager);
         }
     }
 
@@ -618,15 +651,136 @@ public class DocumentsStorageProvider extends DocumentsProvider {
         return !(cancellationSignal != null && cancellationSignal.isCanceled());
     }
 
-    List<OCFile> findFiles(OCFile root, String query) {
-        List<OCFile> result = new ArrayList<>();
-        for (OCFile f : currentStorageManager.getFolderContent(root, false)) {
+    private List<Document> findFiles(Document root, String query) {
+        FileDataStorageManager storageManager = root.getStorageManager();
+        List<Document> result = new ArrayList<>();
+        for (OCFile f : storageManager.getFolderContent(root.getFile(), false)) {
             if (f.isFolder()) {
-                result.addAll(findFiles(f, query));
+                result.addAll(findFiles(new Document(storageManager, f), query));
             } else if (f.getFileName().contains(query)) {
-                result.add(f);
+                result.add(new Document(storageManager, f));
             }
         }
         return result;
     }
+
+    private Uri toNotifyUri(Document document) {
+        return DocumentsContract.buildDocumentUri(
+            getContext().getString(R.string.document_provider_authority),
+            document.getDocumentId());
+    }
+
+    private Document toDocument(String documentId) throws FileNotFoundException {
+        String[] separated = documentId.split(DOCUMENTID_SEPARATOR, DOCUMENTID_PARTS);
+        if (separated.length != DOCUMENTID_PARTS) {
+            throw new FileNotFoundException("Invalid documentID " + documentId + "!");
+        }
+
+        FileDataStorageManager storageManager = rootIdToStorageManager.get(Integer.parseInt(separated[0]));
+        if (storageManager == null) {
+            throw new FileNotFoundException("No storage manager associated for " + documentId + "!");
+        }
+
+        return new Document(storageManager, Long.parseLong(separated[1]));
+    }
+
+    public interface OnTaskFinishedCallback {
+        void onTaskFinished(RemoteOperationResult result);
+    }
+
+    static class ReloadFolderDocumentTask extends AsyncTask<Void, Void, RemoteOperationResult> {
+
+        private final Document folder;
+        private final OnTaskFinishedCallback callback;
+
+        ReloadFolderDocumentTask(Document folder, OnTaskFinishedCallback callback) {
+            this.folder = folder;
+            this.callback = callback;
+        }
+
+        @Override
+        public final RemoteOperationResult doInBackground(Void... params) {
+            Log.d(TAG, "run ReloadFolderDocumentTask(), id=" + folder.getDocumentId());
+            return new RefreshFolderOperation(folder.getFile(), System.currentTimeMillis(), false,
+                                              false, true, folder.getStorageManager(), folder.getAccount(),
+                                              MainApp.getAppContext())
+                .execute(folder.getClient());
+        }
+
+        @Override
+        public final void onPostExecute(RemoteOperationResult result) {
+            if (callback != null) {
+                callback.onTaskFinished(result);
+            }
+        }
+    }
+
+    public class Document {
+        private final FileDataStorageManager storageManager;
+        private final long fileId;
+
+        Document(FileDataStorageManager storageManager, long fileId) {
+            this.storageManager = storageManager;
+            this.fileId = fileId;
+        }
+
+        Document(FileDataStorageManager storageManager, OCFile file) {
+            this.storageManager = storageManager;
+            this.fileId = file.getFileId();
+        }
+
+        Document(FileDataStorageManager storageManager, String filePath) {
+            this.storageManager = storageManager;
+            this.fileId = storageManager.getFileByPath(filePath).getFileId();
+        }
+
+        public String getDocumentId() {
+            for(int i = 0; i < rootIdToStorageManager.size(); i++) {
+                if (Objects.equals(storageManager, rootIdToStorageManager.valueAt(i))) {
+                    return rootIdToStorageManager.keyAt(i) + DOCUMENTID_SEPARATOR + fileId;
+                }
+            }
+            return null;
+        }
+
+        FileDataStorageManager getStorageManager() {
+            return storageManager;
+        }
+
+        public Account getAccount() {
+            return getStorageManager().getAccount();
+        }
+
+        public OCFile getFile() {
+            return getStorageManager().getFileById(fileId);
+        }
+
+        public String getRemotePath() {
+            return getFile().getRemotePath();
+        }
+
+        OwnCloudClient getClient() {
+            try {
+                OwnCloudAccount ocAccount = new OwnCloudAccount(getAccount(), MainApp.getAppContext());
+                return OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, getContext());
+            } catch (OperationCanceledException | IOException | AuthenticatorException |
+                com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) {
+                Log_OC.e(TAG, "Failed to set client", e);
+            }
+            return null;
+        }
+
+        boolean isExpired() {
+            return getFile().getLastSyncDateForData() + CACHE_EXPIRATION < System.currentTimeMillis();
+        }
+
+        Document getParent() {
+            long parentId = getFile().getParentId();
+            if (parentId <= 0) {
+                return null;
+            }
+
+            return new Document(getStorageManager(), parentId);
+        }
+    }
 }

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

@@ -89,6 +89,7 @@ public class FileContentProvider extends ContentProvider {
     private static final String UPGRADE_VERSION_MSG = "OUT of the ADD in onUpgrade; oldVersion == %d, newVersion == %d";
     private static final int SINGLE_PATH_SEGMENT = 1;
     public static final int ARBITRARY_DATA_TABLE_INTRODUCTION_VERSION = 20;
+    public static final int MINIMUM_PATH_SEGMENTS_SIZE = 1;
 
     private DataBaseHelper mDbHelper;
     private Context mContext;
@@ -186,7 +187,7 @@ public class FileContentProvider extends ContentProvider {
             children.close();
         }
 
-        if (uri.getPathSegments().size() > 1) {
+        if (uri.getPathSegments().size() > MINIMUM_PATH_SEGMENTS_SIZE) {
             count += db.delete(ProviderTableMeta.FILE_TABLE_NAME,
                                ProviderTableMeta._ID + "=" + uri.getPathSegments().get(1)
                                    + (!TextUtils.isEmpty(where) ? " AND (" + where + ")" : ""), whereArgs);

+ 0 - 1
src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java

@@ -175,7 +175,6 @@ public class ActivitiesActivity extends FileActivity implements ActivityListInte
         LinearLayoutManager layoutManager = new LinearLayoutManager(this);
 
         recyclerView.setLayoutManager(layoutManager);
-        recyclerView.addItemDecoration(new StickyHeaderItemDecoration(adapter));
         recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
 
             @Override

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

@@ -40,16 +40,6 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
      */
     private OCCapability mCapabilities;
 
-    /**
-     * Flag to signal when the value of mAccount was set.
-     */
-    protected boolean mAccountWasSet;
-
-    /**
-     * Flag to signal when the value of mAccount was restored from a saved state.
-     */
-    protected boolean mAccountWasRestored;
-
     /**
      * Access point to the cached database for the current ownCloud {@link Account}.
      */
@@ -147,20 +137,14 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
      *
      * If not valid, tries to swap it for other valid and existing ownCloud {@link Account}.
      *
-     * POSTCONDITION: updates {@link #mAccountWasSet} and {@link #mAccountWasRestored}.
-     *
      * @param account      New {@link Account} to set.
      * @param savedAccount When 'true', account was retrieved from a saved instance state.
      */
+    @Deprecated
     protected void setAccount(Account account, boolean savedAccount) {
-        Account oldAccount = mCurrentAccount;
-        boolean validAccount =
-                account != null && accountManager.setCurrentOwnCloudAccount(account.name);
+        boolean validAccount = account != null && accountManager.setCurrentOwnCloudAccount(account.name);
         if (validAccount) {
             mCurrentAccount = account;
-            mAccountWasSet = true;
-            mAccountWasRestored = savedAccount || mCurrentAccount.equals(oldAccount);
-
         } else {
             swapToDefaultAccount();
         }
@@ -171,8 +155,6 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
      *
      * If no valid ownCloud {@link Account} exists, then the user is requested
      * to create a new ownCloud {@link Account}.
-     *
-     * POSTCONDITION: updates {@link #mAccountWasSet} and {@link #mAccountWasRestored}.
      */
     protected void swapToDefaultAccount() {
         // default to the most recently used account
@@ -180,12 +162,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
         if (newAccount == null) {
             /// no account available: force account creation
             createAccount(true);
-            mAccountWasSet = false;
-            mAccountWasRestored = false;
-
         } else {
-            mAccountWasSet = true;
-            mAccountWasRestored = newAccount.equals(mCurrentAccount);
             mCurrentAccount = newAccount;
         }
     }
@@ -212,7 +189,8 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
      *
      * Child classes must grant that state depending on the {@link Account} is updated.
      */
-    protected void onAccountSet(boolean stateWasRecovered) {
+    @Deprecated
+    protected void onAccountSet() {
         if (getAccount() != null) {
             mStorageManager = new FileDataStorageManager(getAccount(), getContentResolver());
             mCapabilities = mStorageManager.getCapability(mCurrentAccount.name);
@@ -221,6 +199,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
         }
     }
 
+    @Deprecated
     protected void setAccount(Account account) {
         mCurrentAccount = account;
     }
@@ -250,8 +229,8 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
     protected void onStart() {
         super.onStart();
 
-        if (mAccountWasSet) {
-            onAccountSet(mAccountWasRestored);
+        if(mCurrentAccount != null) {
+            onAccountSet();
         }
     }
 

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

@@ -82,8 +82,8 @@ public class ConflictsResolveActivity extends FileActivity implements OnConflict
     }
 
     @Override
-    protected void onAccountSet(boolean stateWasRecovered) {
-        super.onAccountSet(stateWasRecovered);
+    protected void onStart() {
+        super.onStart();
         if (getAccount() != null) {
             OCFile file = getFile();
             if (getFile() == null) {

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

@@ -293,7 +293,6 @@ public abstract class DrawerActivity extends ToolbarActivity
      * initializes and sets up the drawer header.
      */
     private void setupDrawerHeader() {
-        mIsAccountChooserActive = false;
         mAccountMiddleAccountAvatar = (ImageView) findNavigationViewChildById(R.id.drawer_account_middle);
         mAccountEndAccountAvatar = (ImageView) findNavigationViewChildById(R.id.drawer_account_end);
 

+ 36 - 30
src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java

@@ -65,7 +65,6 @@ public class ExternalSiteWebView extends FileActivity {
     private boolean showSidebar;
     String url;
 
-    @SuppressLint("SetJavaScriptEnabled")
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         Log_OC.v(TAG, "onCreate() start");
@@ -93,7 +92,6 @@ public class ExternalSiteWebView extends FileActivity {
         webview.setFocusableInTouchMode(true);
         webview.setClickable(true);
 
-
         // allow debugging (when building the debug version); see details in
         // https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT &&
@@ -114,17 +112,35 @@ public class ExternalSiteWebView extends FileActivity {
             setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
         }
 
-        ActionBar actionBar = getSupportActionBar();
-        if (actionBar != null) {
-            ThemeUtils.setColoredTitle(actionBar, title, this);
+        setupActionBar(title);
+        setupWebSettings(webSettings);
 
-            if (showSidebar) {
-                actionBar.setDisplayHomeAsUpEnabled(true);
-            } else {
-                setDrawerIndicatorEnabled(false);
-            }
+        final ProgressBar progressBar = findViewById(R.id.progressBar);
+
+        if (progressBar != null) {
+            webview.setWebChromeClient(new WebChromeClient() {
+                public void onProgressChanged(WebView view, int progress) {
+                    progressBar.setProgress(progress * 1000);
+                }
+            });
         }
 
+        webview.setWebViewClient(new WebViewClient() {
+            public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+                InputStream resources = getResources().openRawResource(R.raw.custom_error);
+                String customError = DisplayUtils.getData(resources);
+
+                if (!customError.isEmpty()) {
+                    webview.loadData(customError, "text/html; charset=UTF-8", null);
+                }
+            }
+        });
+
+        webview.loadUrl(url);
+    }
+
+    @SuppressLint("SetJavaScriptEnabled")
+    private void setupWebSettings(WebSettings webSettings) {
         // enable zoom
         webSettings.setSupportZoom(true);
         webSettings.setBuiltInZoomControls(true);
@@ -147,29 +163,19 @@ public class ExternalSiteWebView extends FileActivity {
         // enable javascript
         webSettings.setJavaScriptEnabled(true);
         webSettings.setDomStorageEnabled(true);
+    }
 
-        final ProgressBar progressBar = findViewById(R.id.progressBar);
-
-        if (progressBar != null) {
-            webview.setWebChromeClient(new WebChromeClient() {
-                public void onProgressChanged(WebView view, int progress) {
-                    progressBar.setProgress(progress * 1000);
-                }
-            });
-        }
-
-        webview.setWebViewClient(new WebViewClient() {
-            public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
-                InputStream resources = getResources().openRawResource(R.raw.custom_error);
-                String customError = DisplayUtils.getData(resources);
+    private void setupActionBar(String title) {
+        ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            ThemeUtils.setColoredTitle(actionBar, title, this);
 
-                if (!customError.isEmpty()) {
-                    webview.loadData(customError, "text/html; charset=UTF-8", null);
-                }
+            if (showSidebar) {
+                actionBar.setDisplayHomeAsUpEnabled(true);
+            } else {
+                setDrawerIndicatorEnabled(false);
             }
-        });
-
-        webview.loadUrl(url);
+        }
     }
 
     @Override

+ 46 - 53
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -214,6 +214,7 @@ public class FileDisplayActivity extends FileActivity
 
     private SearchView searchView;
     private PlayerServiceConnection mPlayerConnection;
+    private Account mLastDisplayedAccount;
 
     @Inject
     AppPreferences preferences;
@@ -231,7 +232,7 @@ public class FileDisplayActivity extends FileActivity
         // Set the default theme to replace the launch screen theme.
         setTheme(R.style.Theme_ownCloud_Toolbar_Drawer);
 
-        super.onCreate(savedInstanceState); // this calls onAccountChanged() when ownCloud Account is valid
+        super.onCreate(savedInstanceState);
 
         /// Load of saved instance state
         if (savedInstanceState != null) {
@@ -418,57 +419,6 @@ public class FileDisplayActivity extends FileActivity
         }
     }
 
-    /**
-     * Called when the ownCloud {@link Account} associated to the Activity was just updated.
-     */
-    @Override
-    protected void onAccountSet(boolean stateWasRecovered) {
-        super.onAccountSet(stateWasRecovered);
-        if (getAccount() != null) {
-            /// Check whether the 'main' OCFile handled by the Activity is contained in the
-            // current Account
-            OCFile file = getFile();
-            // get parent from path
-            String parentPath = "";
-            if (file != null) {
-                if (file.isDown() && file.getLastSyncDateForProperties() == 0) {
-                    // upload in progress - right now, files are not inserted in the local
-                    // cache until the upload is successful get parent from path
-                    parentPath = file.getRemotePath().substring(0,
-                            file.getRemotePath().lastIndexOf(file.getFileName()));
-                    if (getStorageManager().getFileByPath(parentPath) == null) {
-                        file = null; // not able to know the directory where the file is uploading
-                    }
-                } else {
-                    file = getStorageManager().getFileByPath(file.getRemotePath());
-                    // currentDir = null if not in the current Account
-                }
-            }
-            if (file == null) {
-                // fall back to root folder
-                file = getStorageManager().getFileByPath(OCFile.ROOT_PATH);  // never returns null
-            }
-            setFile(file);
-
-            if (mAccountWasSet) {
-                setAccountInDrawer(getAccount());
-                setupDrawer();
-            }
-
-            if (!stateWasRecovered) {
-                Log_OC.d(TAG, "Initializing Fragments in onAccountChanged..");
-                initFragmentsWithFile();
-                if (file.isFolder() && TextUtils.isEmpty(searchQuery)) {
-                    startSyncFolderOperation(file, false);
-                }
-
-            } else {
-                updateFragmentsVisibility(!file.isFolder());
-                updateActionBarTitleAndHomeButton(file.isFolder() ? null : file);
-            }
-        }
-    }
-
     private void switchToSearchFragment(Bundle savedInstanceState) {
         if (savedInstanceState == null) {
             OCFileListFragment listOfFiles = new OCFileListFragment();
@@ -2595,8 +2545,51 @@ public class FileDisplayActivity extends FileActivity
     @Override
     public void onStart() {
         super.onStart();
-        EventBus.getDefault().post(new TokenPushEvent());
+        final Account account = getAccount();
+        if (account != null) {
+            /// Check whether the 'main' OCFile handled by the Activity is contained in the
+            // current Account
+            OCFile file = getFile();
+            // get parent from path
+            String parentPath = "";
+            if (file != null) {
+                if (file.isDown() && file.getLastSyncDateForProperties() == 0) {
+                    // upload in progress - right now, files are not inserted in the local
+                    // cache until the upload is successful get parent from path
+                    parentPath = file.getRemotePath().substring(0,
+                                                                file.getRemotePath().lastIndexOf(file.getFileName()));
+                    if (getStorageManager().getFileByPath(parentPath) == null) {
+                        file = null; // not able to know the directory where the file is uploading
+                    }
+                } else {
+                    file = getStorageManager().getFileByPath(file.getRemotePath());
+                    // currentDir = null if not in the current Account
+                }
+            }
+            if (file == null) {
+                // fall back to root folder
+                file = getStorageManager().getFileByPath(OCFile.ROOT_PATH);  // never returns null
+            }
+            setFile(file);
+
+            setAccountInDrawer(account);
+            setupDrawer();
 
+            final boolean accountChanged = !account.equals(mLastDisplayedAccount);
+            if (accountChanged) {
+                Log_OC.d(TAG, "Initializing Fragments in onAccountChanged..");
+                initFragmentsWithFile();
+                if (file.isFolder() && TextUtils.isEmpty(searchQuery)) {
+                    startSyncFolderOperation(file, false);
+                }
+            } else {
+                updateFragmentsVisibility(!file.isFolder());
+                updateActionBarTitleAndHomeButton(file.isFolder() ? null : file);
+            }
+        }
+        mLastDisplayedAccount = account;
+
+        EventBus.getDefault().post(new TokenPushEvent());
         checkForNewDevVersionNecessary(findViewById(R.id.root_layout), getApplicationContext());
     }
 

+ 6 - 17
src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.java

@@ -19,7 +19,6 @@
 
 package com.owncloud.android.ui.activity;
 
-import android.accounts.Account;
 import android.accounts.AuthenticatorException;
 import android.app.Activity;
 import android.content.BroadcastReceiver;
@@ -33,6 +32,7 @@ import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.util.Log;
+import android.view.ActionMode;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
@@ -156,16 +156,8 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C
     }
 
     @Override
-    protected void onStart() {
-        super.onStart();
-    }
-
-    /**
-     * Called when the ownCloud {@link Account} associated to the Activity was just updated.
-     */
-    @Override
-    protected void onAccountSet(boolean stateWasRecovered) {
-        super.onAccountSet(stateWasRecovered);
+    public void onActionModeStarted(ActionMode mode) {
+        super.onActionModeStarted(mode);
         if (getAccount() != null) {
 
             updateFileFromDB();
@@ -177,12 +169,9 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C
                 folder = getFile();
             }
 
-            if (!stateWasRecovered) {
-                OCFileListFragment listOfFolders = getListOfFilesFragment();
-                listOfFolders.listDirectory(folder, false, false);
-
-                startSyncFolderOperation(folder, false);
-            }
+            OCFileListFragment listOfFolders = getListOfFilesFragment();
+            listOfFolders.listDirectory(folder, false, false);
+            startSyncFolderOperation(folder, false);
 
             updateNavigationElementsInActionBar();
         }

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

@@ -104,6 +104,7 @@ public class ManageAccountsActivity extends FileActivity implements AccountListA
     private Drawable tintedCheck;
 
     private ArbitraryDataProvider arbitraryDataProvider;
+    private boolean multipleAccountsSupported;
 
     @Inject UserAccountManager accountManager;
 
@@ -132,16 +133,18 @@ public class ManageAccountsActivity extends FileActivity implements AccountListA
         }
 
         setAccount(currentAccount);
-        onAccountSet(false);
+        onAccountSet();
 
         arbitraryDataProvider = new ArbitraryDataProvider(getContentResolver());
 
+        multipleAccountsSupported = getResources().getBoolean(R.bool.multiaccount_support);
+
         accountListAdapter = new AccountListAdapter(this,
                                                     accountManager,
                                                     getAccountListItems(),
                                                     tintedCheck,
                                                     this,
-                                                    true);
+                                                    multipleAccountsSupported);
 
         recyclerView.setAdapter(accountListAdapter);
         recyclerView.setLayoutManager(new LinearLayoutManager(this));
@@ -284,7 +287,7 @@ public class ManageAccountsActivity extends FileActivity implements AccountListA
                                       getAccountListItems(),
                                       tintedCheck,
                                       this,
-                                      true
+                                      multipleAccountsSupported
                                   );
                                   recyclerView.setAdapter(accountListAdapter);
                                   runOnUiThread(() -> accountListAdapter.notifyDataSetChanged());
@@ -336,7 +339,8 @@ public class ManageAccountsActivity extends FileActivity implements AccountListA
                                                             accountListItemArray,
                                                             tintedCheck,
                                                             this,
-                                                            true);
+                                                            multipleAccountsSupported
+                );
                 recyclerView.setAdapter(accountListAdapter);
             } else {
                 onBackPressed();

+ 4 - 2
src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.java

@@ -392,11 +392,13 @@ public class NotificationsActivity extends FileActivity implements Notifications
     }
 
     @Override
-    public void onActionCallback(boolean isSuccess, NotificationListAdapter.NotificationViewHolder holder) {
+    public void onActionCallback(boolean isSuccess,
+                                 Notification notification,
+                                 NotificationListAdapter.NotificationViewHolder holder) {
         if (isSuccess) {
             adapter.removeNotification(holder);
         } else {
-            adapter.setButtonEnabled(holder, true);
+            adapter.setButtons(holder, notification);
             DisplayUtils.showSnackMessage(this, getString(R.string.notification_action_failed));
         }
     }

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

@@ -233,12 +233,13 @@ public class ReceiveExternalFilesActivity extends FileActivity
 
     public void changeAccount(Account account) {
         setAccount(account, false);
-        onAccountSet(mAccountWasRestored);
+        initTargetFolder();
+        populateDirectoryList();
     }
 
     @Override
-    protected void onAccountSet(boolean stateWasRecovered) {
-        super.onAccountSet(mAccountWasRestored);
+    protected void onStart() {
+        super.onStart();
         initTargetFolder();
         populateDirectoryList();
     }

+ 9 - 2
src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java

@@ -62,6 +62,7 @@ import com.nextcloud.client.preferences.AppPreferencesImpl;
 import com.owncloud.android.BuildConfig;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
+import com.owncloud.android.authentication.AuthenticatorActivity;
 import com.owncloud.android.authentication.PassCodeManager;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.ExternalLinksProvider;
@@ -114,6 +115,7 @@ public class SettingsActivity extends ThemedPreferenceActivity
     private static final int ACTION_CONFIRM_PASSCODE = 6;
     private static final int ACTION_CONFIRM_DEVICE_CREDENTIALS = 7;
     private static final int ACTION_REQUEST_CODE_DAVDROID_SETUP = 10;
+    private static final int TRUE_VALUE = 1;
 
     private static final String DAV_PATH = "/remote.php/dav";
 
@@ -170,7 +172,7 @@ public class SettingsActivity extends ThemedPreferenceActivity
         setupDetailsCategory(accentColor, preferenceScreen);
 
         // More
-        setupMoreCategory(accentColor, appVersion);
+        setupMoreCategory(accentColor);
 
         // About
         setupAboutCategory(accentColor, appVersion);
@@ -308,7 +310,7 @@ public class SettingsActivity extends ThemedPreferenceActivity
         }
     }
 
-    private void setupMoreCategory(int accentColor, String appVersion) {
+    private void setupMoreCategory(int accentColor) {
         PreferenceCategory preferenceCategoryMore = (PreferenceCategory) findPreference("more");
         preferenceCategoryMore.setTitle(ThemeUtils.getColoredTitle(getString(R.string.prefs_category_more),
                 accentColor));
@@ -759,8 +761,13 @@ public class SettingsActivity extends ThemedPreferenceActivity
             // arguments
             if (serverBaseUri != null) {
                 davDroidLoginIntent.putExtra("url", serverBaseUri.toString() + DAV_PATH);
+
+                davDroidLoginIntent.putExtra("loginFlow", TRUE_VALUE);
+                davDroidLoginIntent.setData(Uri.parse(serverBaseUri.toString() + AuthenticatorActivity.WEB_LOGIN));
+                davDroidLoginIntent.putExtra("davPath", DAV_PATH);
             }
             davDroidLoginIntent.putExtra("username", UserAccountManager.getUsername(account));
+
             startActivityForResult(davDroidLoginIntent, ACTION_REQUEST_CODE_DAVDROID_SETUP);
         } else {
             // DAVdroid not installed

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

@@ -88,8 +88,9 @@ public class ShareActivity extends FileActivity implements ShareFragmentListener
         }
     }
 
-    protected void onAccountSet(boolean stateWasRecovered) {
-        super.onAccountSet(stateWasRecovered);
+    @Override
+    protected void onStart() {
+        super.onStart();
 
         // Load data into the list
         Log_OC.d(TAG, "Refreshing lists on account set");

+ 9 - 2
src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java

@@ -297,14 +297,21 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
             } else if (f2 == null) {
                 return 1;
             } else if (f1.isEnabled() && f2.isEnabled()) {
+                if (f1.getFolderName() == null) {
+                    return -1;
+                }
+                if (f2.getFolderName() == null) {
+                    return 1;
+                }
+
                 return f1.getFolderName().toLowerCase(Locale.getDefault()).compareTo(
                     f2.getFolderName().toLowerCase(Locale.getDefault()));
+            } else if (f1.getFolderName() == null && f2.getFolderName() == null) {
+                return 0;
             } else if (f1.isEnabled()) {
                 return -1;
             } else if (f2.isEnabled()) {
                 return 1;
-            } else if (f1.getFolderName() == null && f2.getFolderName() == null) {
-                return 0;
             } else if (f1.getFolderName() == null) {
                 return -1;
             } else if (f2.getFolderName() == null) {

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

@@ -677,8 +677,8 @@ public class UploadFilesActivity extends FileActivity implements
     }
 
     @Override
-    protected void onAccountSet(boolean stateWasRecovered) {
-        super.onAccountSet(stateWasRecovered);
+    protected void onStart() {
+        super.onStart();
         if (getAccount() != null) {
             if (!mAccountOnCreation.equals(getAccount())) {
                 setResult(RESULT_CANCELED);

+ 10 - 14
src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java

@@ -226,6 +226,16 @@ public class UploadListActivity extends FileActivity {
         swipeListRefreshLayout.setRefreshing(false);
     }
 
+    @Override
+    protected void onStart() {
+        super.onStart();
+        ThemeUtils.setColoredTitle(getSupportActionBar(), R.string.uploads_view_title, this);
+        final Account account = getAccount();
+        if (account != null) {
+            setAccountInDrawer(account);
+        }
+    }
+
     @Override
     protected void onResume() {
         Log_OC.v(TAG, "onResume() start");
@@ -382,20 +392,6 @@ public class UploadListActivity extends FileActivity {
         }
     }
 
-    /**
-     * Called when the ownCloud {@link Account} associated to the Activity was just updated.
-     */
-    @Override
-    protected void onAccountSet(boolean stateWasRecovered) {
-        super.onAccountSet(stateWasRecovered);
-
-        ThemeUtils.setColoredTitle(getSupportActionBar(), R.string.uploads_view_title, this);
-
-        if (mAccountWasSet) {
-            setAccountInDrawer(getAccount());
-        }
-    }
-
     public void onDestroy() {
         super.onDestroy();
         unbinder.unbind();

+ 0 - 77
src/main/java/com/owncloud/android/ui/activity/UploadPathActivity.java

@@ -1,77 +0,0 @@
-/*
- *   ownCloud Android client application
- *
- *   Copyright (C) 2015 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.ui.activity;
-
-import android.accounts.Account;
-import android.os.Bundle;
-import android.view.View.OnClickListener;
-
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.ui.fragment.FileFragment;
-import com.owncloud.android.ui.fragment.OCFileListFragment;
-
-public class UploadPathActivity extends FolderPickerActivity implements FileFragment.ContainerActivity,
-        OnClickListener, OnEnforceableRefreshListener {
-
-    public static final String KEY_INSTANT_UPLOAD_PATH = "INSTANT_UPLOAD_PATH";
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        String instantUploadPath = getIntent().getStringExtra(KEY_INSTANT_UPLOAD_PATH);
-
-        // The caller activity (SettingsActivity) is not a FileActivity, so it has no OCFile, only a path.
-        OCFile folder = new OCFile(instantUploadPath);
-
-        setFile(folder);
-    }
-
-    /**
-     * Called when the ownCloud {@link Account} associated to the Activity was
-     * just updated.
-     */
-    @Override
-    protected void onAccountSet(boolean stateWasRecovered) {
-        super.onAccountSet(stateWasRecovered);
-        if (getAccount() != null) {
-
-            updateFileFromDB();
-
-            OCFile folder = getFile();
-            if (folder == null || !folder.isFolder()) {
-                // fall back to root folder
-                setFile(getStorageManager().getFileByPath(OCFile.ROOT_PATH));
-                folder = getFile();
-            }
-
-            onBrowsedDownTo(folder);
-
-            if (!stateWasRecovered) {
-                OCFileListFragment listOfFolders = getListOfFilesFragment();
-                listOfFolders.listDirectory(folder, false, false);
-
-                startSyncFolderOperation(folder, false);
-            }
-
-            updateNavigationElementsInActionBar();
-        }
-    }
-}

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

@@ -136,7 +136,7 @@ public class UserInfoActivity extends FileActivity implements Injectable {
         unbinder = ButterKnife.bind(this);
 
         setAccount(getUserAccountManager().getCurrentAccount());
-        onAccountSet(false);
+        onAccountSet();
 
         boolean useBackgroundImage = URLUtil.isValidUrl(
                 getStorageManager().getCapability(account.name).getServerBackground());

+ 12 - 5
src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java

@@ -141,14 +141,24 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
             downloadIcon(notification.getIcon(), holder.icon);
         }
 
+        setButtons(holder, notification);
+
+        holder.dismiss.setOnClickListener(v -> new DeleteNotificationTask(client, notification, holder,
+                                                                          notificationsActivity).execute());
+    }
+
+    public void setButtons(NotificationViewHolder holder, Notification notification) {
         // add action buttons
         holder.buttons.removeAllViews();
         MaterialButton button;
 
         Resources resources = notificationsActivity.getResources();
-        NotificationExecuteActionTask task = new NotificationExecuteActionTask(client, holder, notificationsActivity);
+        NotificationExecuteActionTask task = new NotificationExecuteActionTask(client,
+                                                                               holder,
+                                                                               notification,
+                                                                               notificationsActivity);
         LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
-            ViewGroup.LayoutParams.WRAP_CONTENT);
+                                                                         ViewGroup.LayoutParams.WRAP_CONTENT);
         params.setMargins(20, 0, 20, 0);
 
         for (Action action : notification.getActions()) {
@@ -185,9 +195,6 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
 
             holder.buttons.addView(button);
         }
-
-        holder.dismiss.setOnClickListener(v -> new DeleteNotificationTask(client, notification, holder,
-                                                                          notificationsActivity).execute());
     }
 
     private SpannableStringBuilder makeSpecialPartsBold(Notification notification) {

+ 4 - 3
src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java

@@ -428,9 +428,10 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                     itemViewHolder.sharedAvatars.removeAllViews();
                 }
 
-                if (onlyOnDevice) {
-                    File localFile = new File(file.getStoragePath());
-
+                // npe fix: looks like file without local storage path somehow get here
+                final String storagePath = file.getStoragePath();
+                if (onlyOnDevice && storagePath != null) {
+                    File localFile = new File(storagePath);
                     long localSize;
                     if (localFile.isDirectory()) {
                         localSize = FileStorageUtils.getFolderSize(localFile);

+ 4 - 0
src/main/java/com/owncloud/android/ui/asynctasks/FetchRemoteFileTask.java

@@ -26,6 +26,7 @@ import android.os.AsyncTask;
 import android.text.TextUtils;
 
 import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
@@ -67,6 +68,9 @@ public class FetchRemoteFileTask extends AsyncTask<Void, Void, String> {
         RemoteOperationResult remoteOperationResult = searchRemoteOperation.execute(account, fileDisplayActivity);
 
         if (remoteOperationResult.isSuccess() && remoteOperationResult.getData() != null) {
+            if (remoteOperationResult.getData().isEmpty()) {
+                return fileDisplayActivity.getString(R.string.remote_file_fetch_failed);
+            }
             String remotePath = ((RemoteFile) remoteOperationResult.getData().get(0)).getRemotePath();
 
             ReadFileRemoteOperation operation = new ReadFileRemoteOperation(remotePath);

+ 7 - 2
src/main/java/com/owncloud/android/ui/asynctasks/NotificationExecuteActionTask.java

@@ -6,6 +6,7 @@ import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.notifications.models.Action;
+import com.owncloud.android.lib.resources.notifications.models.Notification;
 import com.owncloud.android.ui.activity.NotificationsActivity;
 import com.owncloud.android.ui.adapter.NotificationListAdapter;
 
@@ -22,12 +23,16 @@ public class NotificationExecuteActionTask extends AsyncTask<Action, Void, Boole
 
     private NotificationListAdapter.NotificationViewHolder holder;
     private OwnCloudClient client;
+    private Notification notification;
     private NotificationsActivity notificationsActivity;
 
-    public NotificationExecuteActionTask(OwnCloudClient client, NotificationListAdapter.NotificationViewHolder holder,
+    public NotificationExecuteActionTask(OwnCloudClient client,
+                                         NotificationListAdapter.NotificationViewHolder holder,
+                                         Notification notification,
                                          NotificationsActivity notificationsActivity) {
         this.client = client;
         this.holder = holder;
+        this.notification = notification;
         this.notificationsActivity = notificationsActivity;
     }
 
@@ -73,6 +78,6 @@ public class NotificationExecuteActionTask extends AsyncTask<Action, Void, Boole
 
     @Override
     protected void onPostExecute(Boolean isSuccess) {
-        notificationsActivity.onActionCallback(isSuccess, holder);
+        notificationsActivity.onActionCallback(isSuccess, notification, holder);
     }
 }

+ 2 - 4
src/main/java/com/owncloud/android/ui/asynctasks/PrintAsyncTask.java

@@ -81,10 +81,8 @@ public class PrintAsyncTask extends AsyncTask<Void, Void, Boolean> {
         try {
             int status = client.executeMethod(getMethod);
             if (status == HttpStatus.SC_OK) {
-                if (file.exists()) {
-                    if (!file.delete()) {
-                        return false;
-                    }
+                if (file.exists() && !file.delete()) {
+                    return false;
                 }
 
                 file.getParentFile().mkdirs();

+ 0 - 95
src/main/java/com/owncloud/android/ui/components/CustomEditText.java

@@ -1,95 +0,0 @@
-/**
- * Nextcloud Android client application
- *
- * @author Mario Danic
- * Copyright (C) 2017 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.ui.components;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-
-import com.google.android.material.textfield.TextInputEditText;
-import com.owncloud.android.R;
-import com.owncloud.android.authentication.AuthenticatorActivity;
-
-import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
-
-/**
- * Custom edit text to support fixed suffix or prefix
- */
-public class CustomEditText extends TextInputEditText {
-    private Rect fixedRect = new Rect();
-    private String fixedText = "";
-    private boolean isPrefixFixed;
-
-    public CustomEditText(Context context, AttributeSet attrs) {
-        super(context, attrs);
-
-        String serverInputType = getResources().getString(R.string.server_input_type);
-
-        if (AuthenticatorActivity.DIRECTORY_SERVER_INPUT_TYPE.equals(serverInputType)) {
-            isPrefixFixed = true;
-            fixedText = getResources().getString(R.string.server_url) + PATH_SEPARATOR;
-        } else if (AuthenticatorActivity.SUBDOMAIN_SERVER_INPUT_TYPE.equals(serverInputType)) {
-            isPrefixFixed = false;
-            fixedText = "." + getResources().getString(R.string.server_url);
-        }
-
-        if (TextUtils.isEmpty(fixedText)) {
-            setHint(R.string.auth_host_url);
-        }
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        if (!TextUtils.isEmpty(fixedText)) {
-            getPaint().getTextBounds(fixedText, 0, fixedText.length(), fixedRect);
-        }
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-    }
-
-    @Override
-    protected void onDraw(Canvas canvas) {
-        super.onDraw(canvas);
-        if (!getText().toString().startsWith(AuthenticatorActivity.HTTP_PROTOCOL)
-                && !getText().toString().startsWith(AuthenticatorActivity.HTTPS_PROTOCOL)
-                && !TextUtils.isEmpty(fixedText)) {
-            if (isPrefixFixed) {
-                canvas.drawText(fixedText,
-                        super.getCompoundPaddingLeft(),
-                        getBaseline(),
-                        getPaint());
-            } else {
-                canvas.drawText(fixedText, super.getCompoundPaddingLeft()
-                        + getPaint().measureText(getText().toString()), getBaseline(), getPaint());
-            }
-        }
-    }
-
-    @Override
-    public int getCompoundPaddingLeft() {
-        if (!TextUtils.isEmpty(fixedText) && isPrefixFixed) {
-            return super.getCompoundPaddingLeft() + fixedRect.width();
-        } else {
-            return super.getCompoundPaddingLeft();
-        }
-    }
-}

+ 7 - 2
src/main/java/com/owncloud/android/ui/dialog/NoteDialogFragment.java

@@ -137,8 +137,13 @@ public class NoteDialogFragment extends DialogFragment implements DialogInterfac
             ComponentsGetter componentsGetter = (ComponentsGetter) getActivity();
 
             if (componentsGetter != null) {
-                componentsGetter.getFileOperationsHelper().updateNoteToShare(share,
-                    noteEditText.getText().toString().trim());
+                String note = "";
+
+                if (noteEditText.getText() != null) {
+                    note = noteEditText.getText().toString().trim();
+                }
+
+                componentsGetter.getFileOperationsHelper().updateNoteToShare(share, note);
             } else {
                 DisplayUtils.showSnackMessage(requireActivity(), R.string.note_could_not_sent);
             }

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

@@ -56,6 +56,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.di.Injectable;
 import com.nextcloud.client.preferences.AppPreferences;
+import com.nextcloud.client.preferences.AppPreferencesImpl;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -136,7 +137,7 @@ public class ExtendedListFragment extends Fragment implements
     protected SearchView searchView;
     private Handler handler = new Handler(Looper.getMainLooper());
 
-    private float mScale = -1f;
+    private float mScale = AppPreferencesImpl.DEFAULT_GRID_COLUMN;
 
     @Parcel
     public enum SearchType {
@@ -461,8 +462,6 @@ public class ExtendedListFragment extends Fragment implements
             mTops = new ArrayList<>();
             mHeightCell = 0;
         }
-
-        mScale = preferences.getGridColumns();
     }
 
 
@@ -481,6 +480,9 @@ public class ExtendedListFragment extends Fragment implements
     }
 
     public int getColumnsCount() {
+        if (mScale == -1) {
+            return Math.round(AppPreferencesImpl.DEFAULT_GRID_COLUMN);
+        }
         return Math.round(mScale);
     }
 

+ 17 - 3
src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java

@@ -292,13 +292,27 @@ public class FileDetailActivitiesFragment extends Fragment implements
      * @param lastGiven int; -1 to disable
      */
     private void fetchAndSetData(int lastGiven) {
-        final Account currentAccount = accountManager.getCurrentAccount();
-        final Context context = MainApp.getAppContext();
         final FragmentActivity activity = getActivity();
 
+        if (activity == null) {
+            Log_OC.e(this, "Activity is null, aborting!");
+            return;
+        }
+
         final SwipeRefreshLayout empty = swipeEmptyListRefreshLayout;
         final SwipeRefreshLayout list = swipeListRefreshLayout;
+        final Account currentAccount = accountManager.getCurrentAccount();
 
+        if (currentAccount == null) {
+            activity.runOnUiThread(() -> {
+                setEmptyContent(getString(R.string.common_error), getString(R.string.file_detail_activity_error));
+                list.setVisibility(View.GONE);
+                empty.setVisibility(View.VISIBLE);
+            });
+            return;
+        }
+
+        final Context context = MainApp.getAppContext();
 
         Thread t = new Thread(() -> {
             OwnCloudAccount ocAccount;
@@ -372,7 +386,7 @@ public class FileDetailActivitiesFragment extends Fragment implements
 
                 hideRefreshLayoutLoader(activity);
             } catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException | IOException |
-                    OperationCanceledException | AuthenticatorException e) {
+                OperationCanceledException | AuthenticatorException | NullPointerException e) {
                 Log_OC.e(TAG, "Error fetching file details activities", e);
             }
         });

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

@@ -53,10 +53,10 @@ public class PhotoFragment extends OCFileListFragment {
     private SearchRemoteOperation searchRemoteOperation;
     private AsyncTask photoSearchTask;
     private SearchEvent searchEvent;
-    private boolean refresh = false;
+    private boolean refresh;
 
     public PhotoFragment() {
-
+        this.refresh = false;
     }
 
     public PhotoFragment(boolean refresh) {

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

@@ -84,6 +84,7 @@ import org.jetbrains.annotations.NotNull;
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashSet;
@@ -500,6 +501,11 @@ public class ContactListFragment extends FileFragment implements Injectable {
         public String toString() {
             return displayName;
         }
+
+        @Override
+        public int hashCode() {
+            return Arrays.hashCode(new Object[] {displayName, name, type});
+        }
     }
 
     private class DownloadFinishReceiver extends BroadcastReceiver {

+ 4 - 1
src/main/java/com/owncloud/android/ui/notifications/NotificationsContract.java

@@ -21,6 +21,7 @@
 
 package com.owncloud.android.ui.notifications;
 
+import com.owncloud.android.lib.resources.notifications.models.Notification;
 import com.owncloud.android.ui.adapter.NotificationListAdapter;
 
 public interface NotificationsContract {
@@ -32,6 +33,8 @@ public interface NotificationsContract {
 
         void onRemovedAllNotifications(boolean isSuccess);
 
-        void onActionCallback(boolean isSuccess, NotificationListAdapter.NotificationViewHolder holder);
+        void onActionCallback(boolean isSuccess,
+                              Notification notification,
+                              NotificationListAdapter.NotificationViewHolder holder);
     }
 }

+ 28 - 33
src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.java

@@ -183,6 +183,34 @@ public class PreviewImageActivity extends FileActivity implements
     @Override
     public void onStart() {
         super.onStart();
+        if (getAccount() != null) {
+            OCFile file = getFile();
+            /// Validate handled file (first image to preview)
+            if (file == null) {
+                throw new IllegalStateException("Instanced with a NULL OCFile");
+            }
+            if (!MimeTypeUtil.isImage(file)) {
+                throw new IllegalArgumentException("Non-image file passed as argument");
+            }
+
+            // Update file according to DB file, if it is possible
+            if (file.getFileId() > FileDataStorageManager.ROOT_PARENT_ID) {
+                file = getStorageManager().getFileById(file.getFileId());
+            }
+
+            if (file != null) {
+                /// Refresh the activity according to the Account and OCFile set
+                setFile(file);  // reset after getting it fresh from storageManager
+                getSupportActionBar().setTitle(getFile().getFileName());
+                //if (!stateWasRecovered) {
+                initViewPager();
+                //}
+
+            } else {
+                // handled file not in the current Account
+                finish();
+            }
+        }
     }
 
     @Override
@@ -507,39 +535,6 @@ public class PreviewImageActivity extends FileActivity implements
         hideSystemUI(mFullScreenAnchorView);
     }
 
-    @Override
-    protected void onAccountSet(boolean stateWasRecovered) {
-        super.onAccountSet(stateWasRecovered);
-        if (getAccount() != null) {
-            OCFile file = getFile();
-            /// Validate handled file (first image to preview)
-            if (file == null) {
-                throw new IllegalStateException("Instanced with a NULL OCFile");
-            }
-            if (!MimeTypeUtil.isImage(file)) {
-                throw new IllegalArgumentException("Non-image file passed as argument");
-            }
-
-            // Update file according to DB file, if it is possible
-            if (file.getFileId() > FileDataStorageManager.ROOT_PARENT_ID) {
-                file = getStorageManager().getFileById(file.getFileId());
-            }
-
-            if (file != null) {
-                /// Refresh the activity according to the Account and OCFile set
-                setFile(file);  // reset after getting it fresh from storageManager
-                getSupportActionBar().setTitle(getFile().getFileName());
-                //if (!stateWasRecovered) {
-                    initViewPager();
-                //}
-
-            } else {
-                // handled file not in the current Account
-                finish();
-            }
-        }
-    }
-
     @Override
     public void onBrowsedDownTo(OCFile folder) {
         // TODO Auto-generated method stub

+ 0 - 1
src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java

@@ -79,7 +79,6 @@ import javax.inject.Inject;
 import androidx.annotation.DrawableRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
-import androidx.fragment.app.Fragment;
 
 /**
  * This fragment shows a preview of a downloaded media file (audio or video).

+ 65 - 6
src/main/java/com/owncloud/android/ui/preview/PreviewTextFragment.java

@@ -20,10 +20,16 @@
 package com.owncloud.android.ui.preview;
 
 import android.accounts.Account;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Handler;
 import android.text.Html;
+import android.text.Spanned;
+import android.text.TextPaint;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
@@ -48,6 +54,7 @@ import com.owncloud.android.ui.fragment.FileFragment;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.MimeTypeUtil;
 import com.owncloud.android.utils.StringUtils;
+import com.owncloud.android.utils.ThemeUtils;
 
 import org.mozilla.universalchardet.ReaderFactory;
 
@@ -66,7 +73,27 @@ import javax.inject.Inject;
 import androidx.annotation.NonNull;
 import androidx.appcompat.widget.SearchView;
 import androidx.core.view.MenuItemCompat;
-
+import io.noties.markwon.AbstractMarkwonPlugin;
+import io.noties.markwon.Markwon;
+import io.noties.markwon.core.MarkwonTheme;
+import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
+import io.noties.markwon.ext.tables.TablePlugin;
+import io.noties.markwon.ext.tasklist.TaskListDrawable;
+import io.noties.markwon.ext.tasklist.TaskListPlugin;
+import io.noties.markwon.html.HtmlPlugin;
+import io.noties.markwon.syntax.Prism4jTheme;
+import io.noties.markwon.syntax.Prism4jThemeDefault;
+import io.noties.markwon.syntax.SyntaxHighlightPlugin;
+import io.noties.prism4j.Prism4j;
+import io.noties.prism4j.annotations.PrismBundle;
+
+@PrismBundle(
+    include = {
+        "c", "clike", "clojure", "cpp", "csharp", "css", "dart", "git", "go", "groovy", "java", "javascript", "json",
+        "kotlin", "latex", "makefile", "markdown", "markup", "python", "scala", "sql", "swift", "yaml"
+    },
+    grammarLocatorClassName = ".MarkwonGrammarLocator"
+)
 public class PreviewTextFragment extends FileFragment implements SearchView.OnQueryTextListener, Injectable {
     private static final String EXTRA_FILE = "FILE";
     private static final String EXTRA_ACCOUNT = "ACCOUNT";
@@ -116,12 +143,9 @@ public class PreviewTextFragment extends FileFragment implements SearchView.OnQu
         super.onCreateView(inflater, container, savedInstanceState);
         Log_OC.e(TAG, "onCreateView");
 
-
         View ret = inflater.inflate(R.layout.text_file_preview, container, false);
         mTextPreview = ret.findViewById(R.id.text_preview);
 
-        mTextPreview = ret.findViewById(R.id.text_preview);
-
         mMultiView = ret.findViewById(R.id.multi_view);
 
         setupMultiView(ret);
@@ -243,7 +267,7 @@ public class PreviewTextFragment extends FileFragment implements SearchView.OnQu
                         mTextPreview.setText(Html.fromHtml(coloredText.replace("\n", "<br \\>")));
                     }
                 } else {
-                    mTextPreview.setText(mOriginalText);
+                    setText(mTextPreview, mOriginalText, getFile());
                 }
             }, delay);
         }
@@ -253,6 +277,31 @@ public class PreviewTextFragment extends FileFragment implements SearchView.OnQu
         }
     }
 
+    private Spanned getRenderedMarkdownText(Context context, String markdown) {
+        Prism4j prism4j = new Prism4j(new MarkwonGrammarLocator());
+        Prism4jTheme prism4jTheme = Prism4jThemeDefault.create();
+        TaskListDrawable drawable = new TaskListDrawable(Color.GRAY, Color.GRAY, Color.WHITE);
+        drawable.setColorFilter(ThemeUtils.primaryColor(context, true), PorterDuff.Mode.SRC_ATOP);
+
+        final Markwon markwon = Markwon.builder(context)
+            .usePlugin(new AbstractMarkwonPlugin() {
+                @Override
+                public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
+                    TextPaint textPaint = new TextPaint();
+                    textPaint.setColorFilter(new PorterDuffColorFilter(ThemeUtils.primaryColor(context), PorterDuff.Mode.SRC_ATOP));
+                    builder.linkColor(ThemeUtils.primaryColor(context, true));
+                }
+            })
+            .usePlugin(TablePlugin.create(context))
+            .usePlugin(TaskListPlugin.create(drawable))
+            .usePlugin(StrikethroughPlugin.create())
+            .usePlugin(HtmlPlugin.create())
+            .usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme))
+            .build();
+
+        return markwon.toMarkdown(markdown);
+    }
+
     /**
      * Reads the file to preview and shows its contents. Too critical to be anonymous.
      */
@@ -324,7 +373,8 @@ public class PreviewTextFragment extends FileFragment implements SearchView.OnQu
             if (textView != null) {
                 mOriginalText = stringWriter.toString();
                 mSearchView.setOnQueryTextListener(PreviewTextFragment.this);
-                textView.setText(mOriginalText);
+
+                setText(textView, mOriginalText, getFile());
 
                 if (mSearchOpen) {
                     mSearchView.setQuery(mSearchQuery, true);
@@ -505,4 +555,13 @@ public class PreviewTextFragment extends FileFragment implements SearchView.OnQu
             }
         });
     }
+
+    private void setText(TextView textView, String text, OCFile file) {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN
+            && MimeTypeUtil.MIMETYPE_TEXT_MARKDOWN.equals(file.getMimeType())) {
+            textView.setText(getRenderedMarkdownText(getContext(), text));
+        } else {
+            textView.setText(text);
+        }
+    }
 }

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

@@ -196,8 +196,8 @@ public class PreviewVideoActivity extends FileActivity implements OnCompletionLi
     }
 
     @Override
-    protected void onAccountSet(boolean stateWasRecovered) {
-        super.onAccountSet(stateWasRecovered);
+    protected void onStart() {
+        super.onStart();
         if (getAccount() != null) {
             OCFile file = getFile();
             /// Validate handled file  (first image to preview)

+ 9 - 7
src/main/java/com/owncloud/android/utils/FileStorageUtils.java

@@ -54,7 +54,6 @@ import java.util.List;
 import java.util.Locale;
 import java.util.TimeZone;
 
-import androidx.annotation.Nullable;
 import androidx.core.app.ActivityCompat;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
@@ -159,14 +158,13 @@ public final class FileStorageUtils {
     /**
      * Returns the InstantUploadFilePath on the nextcloud instance
      *
-     * @param fileName complete file name
      * @param dateTaken: Time in milliseconds since 1970 when the picture was taken.
      * @return instantUpload path, eg. /Camera/2017/01/fileName
      */
-    public static String getInstantUploadFilePath(Locale current,
+    public static String getInstantUploadFilePath(File file,
+                                                  Locale current,
                                                   String remotePath,
-                                                  String subfolder,
-                                                  @Nullable String fileName,
+                                                  String syncedFolderLocalPath,
                                                   long dateTaken,
                                                   Boolean subfolderByDate) {
         String subfolderByDatePath = "";
@@ -174,13 +172,17 @@ public final class FileStorageUtils {
             subfolderByDatePath = getSubPathFromDate(dateTaken, current);
         }
 
+        String relativeSubfolderPath = new File(file.getAbsolutePath().replace(syncedFolderLocalPath, ""))
+            .getParentFile().getAbsolutePath();
+
         // Path must be normalized; otherwise the next RefreshFolderOperation has a mismatch and deletes the local file.
         return (remotePath +
             OCFile.PATH_SEPARATOR +
             subfolderByDatePath +
-            subfolder + // starts with / so no separator is needed
             OCFile.PATH_SEPARATOR +
-            (fileName == null ? "" : fileName))
+            relativeSubfolderPath +
+            OCFile.PATH_SEPARATOR +
+            file.getName())
             .replaceAll(OCFile.PATH_SEPARATOR + "+", OCFile.PATH_SEPARATOR);
     }
 

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

@@ -24,9 +24,6 @@
 package com.owncloud.android.utils;
 
 import android.accounts.Account;
-import android.app.job.JobInfo;
-import android.app.job.JobScheduler;
-import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
@@ -40,6 +37,7 @@ import com.evernote.android.job.JobManager;
 import com.evernote.android.job.JobRequest;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.device.PowerManagementService;
+import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.network.ConnectivityService;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.owncloud.android.MainApp;
@@ -52,7 +50,6 @@ import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.jobs.FilesSyncJob;
-import com.owncloud.android.jobs.NContentObserverJob;
 import com.owncloud.android.jobs.OfflineSyncJob;
 
 import org.lukhnos.nnio.file.FileVisitResult;
@@ -67,8 +64,6 @@ import java.io.IOException;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
-import androidx.annotation.RequiresApi;
-
 import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
 
 /**
@@ -259,7 +254,7 @@ public final class FilesSyncHelper {
         }).start();
     }
 
-    public static void scheduleFilesSyncIfNeeded(Context context) {
+    public static void scheduleFilesSyncIfNeeded(Context context, BackgroundJobManager jobManager) {
         // always run this because it also allows us to perform retries of manual uploads
         new JobRequest.Builder(FilesSyncJob.TAG)
                 .setPeriodic(900000L, 300000L)
@@ -268,7 +263,7 @@ public final class FilesSyncHelper {
                 .schedule();
 
         if (context != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-            scheduleJobOnN();
+            jobManager.scheduleContentObserverJob();
         }
     }
 
@@ -282,29 +277,5 @@ public final class FilesSyncHelper {
                 .schedule();
         }
     }
-
-    @RequiresApi(api = Build.VERSION_CODES.N)
-    public static void scheduleJobOnN() {
-        JobScheduler jobScheduler = MainApp.getAppContext().getSystemService(JobScheduler.class);
-
-        if (jobScheduler != null) {
-            JobInfo.Builder builder = new JobInfo.Builder(ContentSyncJobId, new ComponentName(MainApp.getAppContext(),
-                    NContentObserverJob.class.getName()));
-            builder.addTriggerContentUri(new JobInfo.TriggerContentUri(android.provider.MediaStore.
-                    Images.Media.INTERNAL_CONTENT_URI,
-                    JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
-            builder.addTriggerContentUri(new JobInfo.TriggerContentUri(MediaStore.
-                    Images.Media.EXTERNAL_CONTENT_URI,
-                    JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
-            builder.addTriggerContentUri(new JobInfo.TriggerContentUri(android.provider.MediaStore.
-                    Video.Media.INTERNAL_CONTENT_URI,
-                    JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
-            builder.addTriggerContentUri(new JobInfo.TriggerContentUri(MediaStore.
-                    Video.Media.EXTERNAL_CONTENT_URI,
-                    JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
-            builder.setTriggerContentMaxDelay(1500);
-            jobScheduler.schedule(builder.build());
-        }
-    }
 }
 

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

@@ -72,6 +72,7 @@ public final class MimeTypeUtil {
     private static final Map<String, Integer> MAIN_MIMETYPE_TO_ICON_MAPPING = new HashMap<>();
     /** Mapping: mime type for file extension. */
     private static final Map<String, List<String>> FILE_EXTENSION_TO_MIMETYPE_MAPPING = new HashMap<>();
+    public static final String MIMETYPE_TEXT_MARKDOWN = "text/markdown";
 
     static {
         populateFileExtensionMimeTypeMapping();
@@ -648,12 +649,12 @@ public final class MimeTypeUtil {
         FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("m4a", Collections.singletonList("audio/mp4"));
         FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("m4b", Collections.singletonList("audio/m4b"));
         FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("m4v", Collections.singletonList("video/mp4"));
-        FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("markdown", Collections.singletonList("text/markdown"));
-        FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("mdown", Collections.singletonList("text/markdown"));
-        FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("md", Collections.singletonList("text/markdown"));
+        FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("markdown", Collections.singletonList(MIMETYPE_TEXT_MARKDOWN));
+        FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("mdown", Collections.singletonList(MIMETYPE_TEXT_MARKDOWN));
+        FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("md", Collections.singletonList(MIMETYPE_TEXT_MARKDOWN));
         FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("mdb", Collections.singletonList("application/msaccess"));
-        FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("mdwn", Collections.singletonList("text/markdown"));
-        FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("mkd", Collections.singletonList("text/markdown"));
+        FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("mdwn", Collections.singletonList(MIMETYPE_TEXT_MARKDOWN));
+        FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("mkd", Collections.singletonList(MIMETYPE_TEXT_MARKDOWN));
         FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("mef", Collections.singletonList("image/x-dcraw"));
         FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("mkv", Collections.singletonList("video/x-matroska"));
         FILE_EXTENSION_TO_MIMETYPE_MAPPING.put("mobi", Collections.singletonList("application/x-mobipocket-ebook"));

+ 33 - 3
src/main/java/org/nextcloud/providers/cursors/FileCursor.java

@@ -22,10 +22,13 @@ package org.nextcloud.providers.cursors;
 
 import android.annotation.TargetApi;
 import android.database.MatrixCursor;
+import android.os.AsyncTask;
 import android.os.Build;
+import android.os.Bundle;
 import android.provider.DocumentsContract.Document;
 
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.providers.DocumentsStorageProvider;
 import com.owncloud.android.utils.MimeTypeUtil;
 
 @TargetApi(Build.VERSION_CODES.KITKAT)
@@ -37,15 +40,42 @@ public class FileCursor extends MatrixCursor {
             Document.COLUMN_FLAGS, Document.COLUMN_LAST_MODIFIED
     };
 
+    private Bundle extra;
+    private AsyncTask<?, ?, ?> loadingTask;
+
     public FileCursor(String... projection) {
         super(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
     }
 
-    public void addFile(OCFile file) {
-        if (file == null) {
+    public void setLoadingTask(AsyncTask<?, ?, ?> task) {
+        this.loadingTask = task;
+    }
+
+    @Override
+    public void setExtras(Bundle extras) {
+        this.extra = extras;
+    }
+
+    @Override
+    public Bundle getExtras() {
+        return extra;
+    }
+
+    @Override
+    public void close() {
+        super.close();
+        if (loadingTask != null && loadingTask.getStatus() != AsyncTask.Status.FINISHED) {
+            loadingTask.cancel(false);
+        }
+    }
+
+    public void addFile(DocumentsStorageProvider.Document document) {
+        if (document == null) {
             return;
         }
 
+        OCFile file = document.getFile();
+
         final int iconRes = MimeTypeUtil.getFileTypeIconId(file.getMimeType(), file.getFileName());
         final String mimeType = file.isFolder() ? Document.MIME_TYPE_DIR : file.getMimeType();
         int flags = Document.FLAG_SUPPORTS_DELETE |
@@ -64,7 +94,7 @@ public class FileCursor extends MatrixCursor {
             flags = Document.FLAG_SUPPORTS_RENAME | flags;
         }
 
-        newRow().add(Document.COLUMN_DOCUMENT_ID, Long.toString(file.getFileId()))
+        newRow().add(Document.COLUMN_DOCUMENT_ID, document.getDocumentId())
                 .add(Document.COLUMN_DISPLAY_NAME, file.getFileName())
                 .add(Document.COLUMN_LAST_MODIFIED, file.getModificationTimestamp())
                 .add(Document.COLUMN_SIZE, file.getFileLength())

+ 10 - 10
src/main/java/org/nextcloud/providers/cursors/RootCursor.java

@@ -28,10 +28,7 @@ import android.os.Build;
 import android.provider.DocumentsContract.Root;
 
 import com.owncloud.android.R;
-import com.owncloud.android.datamodel.FileDataStorageManager;
-import com.owncloud.android.datamodel.OCFile;
-
-import static com.owncloud.android.datamodel.OCFile.ROOT_PATH;
+import com.owncloud.android.providers.DocumentsStorageProvider;
 
 
 @TargetApi(Build.VERSION_CODES.KITKAT)
@@ -47,16 +44,19 @@ public class RootCursor extends MatrixCursor {
         super(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
     }
 
-    public void addRoot(Account account, Context context) {
-        final FileDataStorageManager manager = new FileDataStorageManager(account, context.getContentResolver());
-        final OCFile mainDir = manager.getFileByPath(ROOT_PATH);
+    public void addRoot(DocumentsStorageProvider.Document document, Context context) {
+        Account account = document.getAccount();
+
+        int rootFlags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH;
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+            rootFlags = rootFlags | Root.FLAG_SUPPORTS_IS_CHILD;
+        }
 
         newRow().add(Root.COLUMN_ROOT_ID, account.name)
-            .add(Root.COLUMN_DOCUMENT_ID, mainDir.getFileId())
+            .add(Root.COLUMN_DOCUMENT_ID, document.getDocumentId())
             .add(Root.COLUMN_SUMMARY, account.name)
             .add(Root.COLUMN_TITLE, context.getString(R.string.app_name))
             .add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
-            .add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_RECENTS |
-                Root.FLAG_SUPPORTS_SEARCH);
+            .add(Root.COLUMN_FLAGS, rootFlags);
     }
 }

+ 227 - 257
src/main/res/layout-land/account_setup.xml

@@ -1,257 +1,227 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ownCloud Android client application
-
-  Copyright (C) 2012  Bartek Przybylski
-  Copyright (C) 2015-2016 ownCloud Inc.
-  Copyright (C) 2016 Nextcloud
-
-  This program is free software: you can redistribute it and/or modify
-  it under the terms of the GNU General Public License version 2,
-  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/>.
--->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-	android:padding="@dimen/standard_padding"
-    >
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_above="@+id/bottom_block"
-        android:layout_alignParentTop="true"
-        android:orientation="horizontal" >
-
-		<ImageView
-			android:id="@+id/thumbnail"
-			android:layout_width="0dp"
-			android:layout_height="wrap_content"
-			android:layout_weight="1"
-			android:layout_gravity="center"
-			android:src="@drawable/logo"
-            android:contentDescription="@string/app_name"
-			/>
-
-		<ScrollView
-		    android:layout_width="0dp"
-		    android:layout_height="match_parent"
-		    android:layout_weight="1"
-		    android:id="@+id/scroll"
-		    android:fillViewport="true"
-		    android:orientation="vertical" >
-
-			<LinearLayout
-			    android:id="@+id/LinearLayout1"
-			    android:layout_width="match_parent"
-			    android:layout_height="wrap_content"
-			    android:gravity="center"
-			    android:orientation="vertical"
-			    android:padding="@dimen/standard_half_padding" >
-
-                <com.google.android.material.button.MaterialButton
-					android:id="@+id/centeredRefreshButton"
-					android:layout_width="wrap_content"
-					android:layout_height="wrap_content"
-					android:layout_gravity="center_horizontal"
-					android:layout_marginBottom="@dimen/alternate_margin"
-					android:theme="@style/Button"
-					android:text="@string/auth_check_server"
-					android:visibility="gone"
-					android:contentDescription="@string/auth_check_server"
-                    app:cornerRadius="@dimen/button_corner_radius"/>
-
-				<TextView
-                    android:id="@+id/instructions_message"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="fill_horizontal"
-                    android:text="@string/auth_expired_basic_auth_toast"
-                    android:visibility="gone"
-                    android:layout_marginBottom="@dimen/alternate_margin"
-                    android:textColor="@color/login_text_color"
-                    android:contentDescription="@string/auth_expired_basic_auth_toast"/>
-
-                <FrameLayout
-	        		android:id="@+id/hostUrlFrame"
-					android:layout_width="match_parent"
-					android:layout_height="wrap_content"
-					android:layout_marginBottom="@dimen/zero"
-                    android:background="@color/primary">
-
-                    <com.google.android.material.textfield.TextInputLayout
-                        android:id="@+id/input_layout_hostUrl"
-                        android:layout_width="match_parent"
-                        android:layout_height="wrap_content"
-                        app:boxBackgroundColor="@color/primary"
-                        app:hintTextColor="@color/fg_inverse">
-
-                        <com.owncloud.android.ui.components.CustomEditText
-                            android:id="@+id/hostUrlInput"
-                            android:layout_width="match_parent"
-                            android:layout_height="wrap_content"
-                            android:layout_gravity="bottom"
-                            android:contentDescription="@string/auth_host_address"
-                            android:drawablePadding="@dimen/alternate_half_padding"
-                            android:inputType="textUri"
-                            android:paddingRight="@dimen/alternate_padding_right"
-                            android:paddingEnd="@dimen/alternate_padding_right"
-                            android:paddingStart="@dimen/zero"
-                            android:paddingLeft="@dimen/zero"
-                            android:textColor="@color/login_text_color"
-                            android:theme="@style/Nextcloud.EditText.Login">
-
-                            <requestFocus/>
-                        </com.owncloud.android.ui.components.CustomEditText>
-
-                    </com.google.android.material.textfield.TextInputLayout>
-
-					<ImageButton
-                        android:id="@+id/testServerButton"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:layout_gravity="center_vertical|end"
-                        android:layout_marginRight="@dimen/alternate_half_padding"
-                        android:layout_marginEnd="@dimen/alternate_half_padding"
-                        android:padding="@dimen/zero"
-                        android:scaleType="fitCenter"
-                        android:src="@drawable/arrow_right"
-                        android:tint="@color/login_btn_tint"
-                        android:background="@android:color/transparent"
-                        android:onClick="onTestServerConnectionClick"
-                        android:contentDescription="@string/test_server_button"
-						/>
-
-					<ImageButton
-                        android:id="@+id/embeddedRefreshButton"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:layout_gravity="center_vertical|end"
-                        android:layout_marginRight="@dimen/alternate_half_margin"
-                        android:layout_marginEnd="@dimen/alternate_half_margin"
-                        android:padding="@dimen/zero"
-                        android:scaleType="fitCenter"
-                        android:src="@drawable/ic_action_refresh"
-                        android:visibility="gone"
-                        android:background="@android:color/transparent"
-                        android:contentDescription="@string/auth_refresh_button"
-					    />
-				</FrameLayout>
-
-				<TextView
-					android:id="@+id/server_status_text"
-					android:layout_width="match_parent"
-					android:layout_height="wrap_content"
-					android:layout_marginBottom="@dimen/alternate_half_margin"
-                    android:drawableLeft="@android:drawable/stat_notify_sync"
-                    android:drawableStart="@android:drawable/stat_notify_sync"
-					android:drawablePadding="@dimen/alternate_half_padding"
-					android:gravity="center_vertical"
-					android:textColor="@color/login_text_color"
-					android:text="@string/auth_testing_connection"
-					android:minHeight="@dimen/display_text_min_height"
-                    android:contentDescription="@string/auth_testing_connection"/>
-
-                <com.google.android.material.textfield.TextInputLayout
-                    android:id="@+id/input_layout_account_username"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    app:boxBackgroundColor="@color/primary"
-                    app:hintTextColor="@color/fg_inverse"
-                    android:visibility="gone">
-
-                    <com.google.android.material.textfield.TextInputEditText
-                        android:id="@+id/account_username"
-                        android:layout_width="match_parent"
-                        android:layout_height="wrap_content"
-                        android:autofillHints="username"
-                        android:ems="10"
-                        android:hint="@string/auth_username"
-                        android:inputType="textNoSuggestions"
-                        android:textColor="@color/login_text_color"
-                        android:textColorHint="@color/login_text_hint_color" />
-                </com.google.android.material.textfield.TextInputLayout>
-
-                <com.google.android.material.textfield.TextInputLayout
-                    android:id="@+id/input_layout_account_password"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    app:boxBackgroundColor="@color/primary"
-                    app:hintTextColor="@color/fg_inverse"
-                    android:visibility="gone"
-                    app:passwordToggleDrawable="@drawable/password_visibility_selector"
-                    app:passwordToggleTint="@color/login_text_hint_color">
-
-                    <com.google.android.material.textfield.TextInputEditText
-                        android:id="@+id/account_password"
-                        android:layout_width="match_parent"
-                        android:layout_height="wrap_content"
-                        android:autofillHints="password"
-                        android:drawablePadding="@dimen/alternate_half_padding"
-                        android:ems="10"
-                        android:hint="@string/auth_password"
-                        android:inputType="textPassword"
-                        android:textColor="@color/login_text_color"
-                        android:textColorHint="@color/login_text_hint_color" />
-                </com.google.android.material.textfield.TextInputLayout>
-
-                <TextView
-					android:id="@+id/auth_status_text"
-					android:layout_width="match_parent"
-					android:layout_height="wrap_content"
-					android:gravity="center_vertical"
-					android:text="@string/auth_unauthorized"
-                    android:drawableLeft="@android:drawable/stat_notify_sync"
-                    android:drawableStart="@android:drawable/stat_notify_sync"
-					android:drawablePadding="@dimen/alternate_half_padding"
-					android:textColor="@color/login_text_color"
-                    android:contentDescription="@string/auth_unauthorized"
-					/>
-
-			</LinearLayout>
-		</ScrollView>
-
-    </LinearLayout>
-
-    <LinearLayout
-        android:id="@+id/bottom_block"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-		android:layout_alignParentBottom="true"
-        android:orientation="vertical"
-        android:paddingTop="@dimen/standard_half_padding">
-
-        <com.google.android.material.button.MaterialButton
-			android:id="@+id/buttonOK"
-			android:theme="@style/Button.Login"
-			style="@style/Button.Login"
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
-			android:padding="@dimen/standard_padding"
-			android:layout_gravity="center_horizontal"
-			android:enabled="false"
-			android:text="@string/setup_btn_connect"
-			android:contentDescription="@string/setup_btn_connect"
-			android:visibility="visible"
-            app:cornerRadius="@dimen/button_corner_radius"/>
-	</LinearLayout>
-
-    <ImageButton
-        android:id="@+id/scanQR"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:src="@drawable/qrcode_scan"
-        android:theme="@style/Button.Login"
-        android:background="@color/transparent"
-        android:contentDescription="@string/scanQR_description" />
-
-</RelativeLayout>
+<?xml version="1.0" encoding="utf-8"?><!--
+  ownCloud Android client application
+
+  Copyright (C) 2012 Bartek Przybylski
+  Copyright (C) 2015-2016 ownCloud Inc.
+  Copyright (C) 2016 Nextcloud
+  Copyright (C) 2019 Tobias Kaminsky
+
+  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/>.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_alignParentTop="true"
+    android:orientation="horizontal"
+    android:padding="@dimen/standard_padding">
+
+    <ImageView
+        android:id="@+id/thumbnail"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_weight="1"
+        android:contentDescription="@string/app_name"
+        android:src="@drawable/logo" />
+
+    <ScrollView
+        android:id="@+id/scroll"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:fillViewport="true"
+        android:orientation="vertical">
+
+        <LinearLayout
+            android:id="@+id/LinearLayout1"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:padding="@dimen/standard_half_padding">
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/centeredRefreshButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:layout_marginBottom="@dimen/alternate_margin"
+                android:contentDescription="@string/auth_check_server"
+                android:text="@string/auth_check_server"
+                android:theme="@style/Button"
+                android:visibility="gone"
+                app:cornerRadius="@dimen/button_corner_radius" />
+
+            <TextView
+                android:id="@+id/instructions_message"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="fill_horizontal"
+                android:layout_marginBottom="@dimen/alternate_margin"
+                android:contentDescription="@string/auth_expired_basic_auth_toast"
+                android:text="@string/auth_expired_basic_auth_toast"
+                android:textColor="@color/login_text_color"
+                android:visibility="gone" />
+
+            <FrameLayout
+                android:id="@+id/hostUrlFrame"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="@dimen/zero"
+                android:background="@color/primary">
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:orientation="vertical">
+
+                    <TextView
+                        android:id="@+id/editText"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:ems="10"
+                        android:text="@string/auth_host_url"
+                        android:textColor="@color/login_text_color" />
+
+                    <com.google.android.material.textfield.TextInputEditText
+                        android:id="@+id/hostUrlInput"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_gravity="bottom"
+                        android:contentDescription="@string/auth_host_address"
+                        android:drawablePadding="@dimen/alternate_half_padding"
+                        android:inputType="textUri"
+                        android:paddingStart="@dimen/zero"
+                        android:paddingLeft="@dimen/zero"
+                        android:paddingEnd="@dimen/alternate_padding_right"
+                        android:paddingRight="@dimen/alternate_padding_right"
+                        android:textColor="@color/login_text_color"
+                        android:textColorHint="@color/login_text_hint_color"
+                        android:theme="@style/Nextcloud.EditText.Login">
+
+                        <requestFocus />
+
+                    </com.google.android.material.textfield.TextInputEditText>
+
+                </LinearLayout>
+
+                <ImageButton
+                    android:id="@+id/testServerButton"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical|end"
+                    android:layout_marginEnd="@dimen/alternate_half_padding"
+                    android:layout_marginRight="@dimen/alternate_half_padding"
+                    android:background="@android:color/transparent"
+                    android:contentDescription="@string/test_server_button"
+                    android:onClick="onTestServerConnectionClick"
+                    android:padding="@dimen/zero"
+                    android:scaleType="fitCenter"
+                    android:src="@drawable/arrow_right"
+                    android:tint="@color/login_btn_tint" />
+
+                <ImageButton
+                    android:id="@+id/embeddedRefreshButton"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical|end"
+                    android:layout_marginEnd="@dimen/alternate_half_margin"
+                    android:layout_marginRight="@dimen/alternate_half_margin"
+                    android:background="@android:color/transparent"
+                    android:contentDescription="@string/auth_refresh_button"
+                    android:padding="@dimen/zero"
+                    android:scaleType="fitCenter"
+                    android:src="@drawable/ic_action_refresh"
+                    android:visibility="gone" />
+            </FrameLayout>
+
+            <TextView
+                android:id="@+id/server_status_text"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="@dimen/alternate_half_margin"
+                android:contentDescription="@string/auth_testing_connection"
+                android:drawableStart="@android:drawable/stat_notify_sync"
+                android:drawableLeft="@android:drawable/stat_notify_sync"
+                android:drawablePadding="@dimen/alternate_half_padding"
+                android:gravity="center_vertical"
+                android:minHeight="@dimen/display_text_min_height"
+                android:text="@string/auth_testing_connection"
+                android:textColor="@color/login_text_color" />
+
+            <com.google.android.material.textfield.TextInputEditText
+                android:id="@+id/account_username"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:autofillHints="username"
+                android:ems="10"
+                android:hint="@string/auth_username"
+                android:inputType="textNoSuggestions"
+                android:textColor="@color/login_text_color"
+                android:textColorHint="@color/login_text_hint_color"
+                android:visibility="gone" />
+
+            <com.google.android.material.textfield.TextInputEditText
+                android:id="@+id/account_password"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:autofillHints="password"
+                android:drawablePadding="@dimen/alternate_half_padding"
+                android:ems="10"
+                android:hint="@string/auth_password"
+                android:inputType="textPassword"
+                android:textColor="@color/login_text_color"
+                android:textColorHint="@color/login_text_hint_color"
+                android:visibility="gone" />
+
+            <TextView
+                android:id="@+id/auth_status_text"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:contentDescription="@string/auth_unauthorized"
+                android:drawableStart="@android:drawable/stat_notify_sync"
+                android:drawableLeft="@android:drawable/stat_notify_sync"
+                android:drawablePadding="@dimen/alternate_half_padding"
+                android:gravity="center_vertical"
+                android:text="@string/auth_unauthorized"
+                android:textColor="@color/login_text_color" />
+
+            <ImageButton
+                android:id="@+id/scanQR"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:background="@color/transparent"
+                android:contentDescription="@string/scanQR_description"
+                android:src="@drawable/qrcode_scan"
+                android:theme="@style/Button.Login" />
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/buttonOK"
+                style="@style/Button.Login"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:contentDescription="@string/setup_btn_connect"
+                android:enabled="false"
+                android:padding="@dimen/standard_padding"
+                android:text="@string/setup_btn_connect"
+                android:theme="@style/Button.Login"
+                android:visibility="gone"
+                app:cornerRadius="@dimen/button_corner_radius" />
+
+        </LinearLayout>
+
+    </ScrollView>
+
+</LinearLayout>

+ 229 - 239
src/main/res/layout/account_setup.xml

@@ -1,239 +1,229 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ownCloud Android client application
-
-  Copyright (C) 2012  Bartek Przybylski
-  Copyright (C) 2015-2016 ownCloud Inc.
-  Copyright (C) 2016 Nextcloud
-
-  This program is free software: you can redistribute it and/or modify
-  it under the terms of the GNU General Public License version 2,
-  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/>.
--->
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
-            xmlns:app="http://schemas.android.com/apk/res-auto"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_gravity="center"
-            android:fillViewport="true"
-            android:orientation="vertical"
-            android:id="@+id/scroll"
-            android:background="@color/primary">
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:gravity="center"
-        android:orientation="vertical"
-        android:padding="@dimen/standard_padding" >
-
-        <ImageView
-            android:id="@+id/thumbnail"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginBottom="@dimen/standard_padding"
-            android:contentDescription="@string/app_name"
-            android:src="@drawable/logo" />
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="302dp"
-            android:gravity="center"
-            android:orientation="vertical"
-            android:padding="@dimen/standard_half_padding">
-
-            <com.google.android.material.button.MaterialButton
-                android:id="@+id/centeredRefreshButton"
-                style="@style/Button.Primary"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center_horizontal"
-                android:layout_marginBottom="@dimen/alternate_margin"
-                android:contentDescription="@string/auth_check_server"
-                android:text="@string/auth_check_server"
-                android:theme="@style/Button"
-                android:visibility="gone"
-                app:cornerRadius="@dimen/button_corner_radius" />
-
-            <TextView
-                android:id="@+id/instructions_message"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="fill_horizontal"
-                android:layout_marginBottom="@dimen/alternate_margin"
-                android:contentDescription="@string/auth_expired_basic_auth_toast"
-                android:text="@string/auth_expired_basic_auth_toast"
-                android:textColor="@color/login_text_color"
-                android:visibility="gone" />
-
-            <FrameLayout
-                android:id="@+id/hostUrlFrame"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginBottom="@dimen/zero"
-                android:background="@color/primary">
-
-                <com.google.android.material.textfield.TextInputLayout
-                    android:id="@+id/input_layout_hostUrl"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    app:boxBackgroundColor="@color/primary"
-                    app:hintTextColor="@color/fg_inverse">
-
-                    <com.owncloud.android.ui.components.CustomEditText
-                        android:id="@+id/hostUrlInput"
-                        android:layout_width="match_parent"
-                        android:layout_height="wrap_content"
-                        android:layout_gravity="bottom"
-                        android:contentDescription="@string/auth_host_address"
-                        android:drawablePadding="@dimen/alternate_half_padding"
-                        android:inputType="textUri"
-                        android:paddingStart="@dimen/zero"
-                        android:paddingLeft="@dimen/zero"
-                        android:paddingEnd="@dimen/alternate_padding_right"
-                        android:paddingRight="@dimen/alternate_padding_right"
-                        android:textColor="@color/login_text_color"
-                        android:theme="@style/Nextcloud.EditText.Login">
-
-                        <requestFocus />
-                    </com.owncloud.android.ui.components.CustomEditText>
-
-                </com.google.android.material.textfield.TextInputLayout>
-
-                <ImageButton
-                    android:id="@+id/testServerButton"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical|end"
-                    android:layout_marginEnd="@dimen/alternate_half_padding"
-                    android:layout_marginRight="@dimen/alternate_half_padding"
-                    android:background="@android:color/transparent"
-                    android:contentDescription="@string/test_server_button"
-                    android:onClick="onTestServerConnectionClick"
-                    android:padding="@dimen/zero"
-                    android:scaleType="fitCenter"
-                    android:src="@drawable/arrow_right"
-                    android:tint="@color/login_text_color" />
-
-                <ImageButton
-                    android:id="@+id/embeddedRefreshButton"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical|end"
-                    android:layout_marginEnd="@dimen/alternate_half_padding"
-                    android:layout_marginRight="@dimen/alternate_half_padding"
-                    android:background="@android:color/transparent"
-                    android:contentDescription="@string/auth_refresh_button"
-                    android:padding="@dimen/zero"
-                    android:scaleType="fitCenter"
-                    android:src="@drawable/ic_action_refresh"
-                    android:visibility="gone" />
-            </FrameLayout>
-
-            <TextView
-                android:id="@+id/server_status_text"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginBottom="@dimen/alternate_margin"
-                android:contentDescription="@string/auth_testing_connection"
-                android:drawableStart="@android:drawable/stat_notify_sync"
-                android:drawableLeft="@android:drawable/stat_notify_sync"
-                android:drawablePadding="@dimen/alternate_half_padding"
-                android:gravity="center_vertical"
-                android:minHeight="@dimen/display_text_min_height"
-                android:text="@string/auth_testing_connection"
-                android:textColor="@color/login_text_color" />
-
-            <com.google.android.material.textfield.TextInputLayout
-                android:id="@+id/input_layout_account_username"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                app:boxBackgroundColor="@color/primary"
-                app:hintTextColor="@color/fg_inverse"
-                android:visibility="gone">
-
-                <com.google.android.material.textfield.TextInputEditText
-                    android:id="@+id/account_username"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:autofillHints="username"
-                    android:ems="10"
-                    android:hint="@string/auth_username"
-                    android:inputType="textNoSuggestions"
-                    android:textColor="@color/login_text_color"
-                    android:theme="@style/Nextcloud.EditText.Login"
-                    android:textColorHint="@color/login_text_hint_color" />
-            </com.google.android.material.textfield.TextInputLayout>
-
-            <com.google.android.material.textfield.TextInputLayout
-                android:id="@+id/input_layout_account_password"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                app:boxBackgroundColor="@color/primary"
-                app:hintTextColor="@color/fg_inverse"
-                android:visibility="gone"
-                app:passwordToggleDrawable="@drawable/password_visibility_selector"
-                app:passwordToggleTint="@color/login_text_hint_color">
-
-                <com.google.android.material.textfield.TextInputEditText
-                    android:id="@+id/account_password"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:autofillHints="password"
-                    android:drawablePadding="@dimen/alternate_half_padding"
-                    android:ems="10"
-                    android:hint="@string/auth_password"
-                    android:inputType="textPassword"
-                    android:textColor="@color/login_text_color"
-                    android:theme="@style/Nextcloud.EditText.Login"
-                    android:textColorHint="@color/login_text_hint_color" />
-            </com.google.android.material.textfield.TextInputLayout>
-
-            <TextView
-                android:id="@+id/auth_status_text"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginBottom="@dimen/alternate_margin"
-                android:contentDescription="@string/auth_unauthorized"
-                android:drawableStart="@android:drawable/stat_notify_sync"
-                android:drawableLeft="@android:drawable/stat_notify_sync"
-                android:drawablePadding="@dimen/alternate_half_padding"
-                android:gravity="center_vertical"
-                android:text="@string/auth_unauthorized"
-                android:textColor="@color/login_text_color" />
-
-            <com.google.android.material.button.MaterialButton
-                android:id="@+id/buttonOK"
-                style="@style/Button.Login"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center_horizontal"
-                android:contentDescription="@string/setup_btn_connect"
-                android:enabled="false"
-                android:padding="@dimen/standard_padding"
-                android:text="@string/setup_btn_connect"
-                android:theme="@style/Button.Login"
-                android:visibility="gone"
-                app:cornerRadius="@dimen/button_corner_radius" />
-        </LinearLayout>
-
-        <ImageButton
-            android:id="@+id/scanQR"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:src="@drawable/qrcode_scan"
-            android:theme="@style/Button.Login"
-            android:background="@color/transparent"
-            android:contentDescription="@string/scanQR_description" />
-
-    </LinearLayout>
-
-</ScrollView>
+<?xml version="1.0" encoding="utf-8"?><!--
+  ownCloud Android client application
+
+  Copyright (C) 2012 Bartek Przybylski
+  Copyright (C) 2015-2016 ownCloud Inc.
+  Copyright (C) 2016 Nextcloud
+  Copyright (C) 2019 Tobias Kaminsky
+
+  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/>.
+-->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/scroll"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_gravity="center"
+    android:fillViewport="true"
+    android:orientation="vertical"
+    android:background="@color/primary">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:orientation="vertical"
+        android:padding="@dimen/standard_padding">
+
+        <ImageView
+            android:id="@+id/thumbnail"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="@dimen/standard_padding"
+            android:contentDescription="@string/app_name"
+            android:src="@drawable/logo" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:padding="@dimen/standard_half_padding">
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/centeredRefreshButton"
+                style="@style/Button.Primary"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:layout_marginBottom="@dimen/alternate_margin"
+                android:contentDescription="@string/auth_check_server"
+                android:text="@string/auth_check_server"
+                android:theme="@style/Button"
+                android:visibility="gone"
+                app:cornerRadius="@dimen/button_corner_radius" />
+
+            <TextView
+                android:id="@+id/instructions_message"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="fill_horizontal"
+                android:layout_marginBottom="@dimen/alternate_margin"
+                android:contentDescription="@string/auth_expired_basic_auth_toast"
+                android:text="@string/auth_expired_basic_auth_toast"
+                android:textColor="@color/login_text_color"
+                android:visibility="gone" />
+
+
+            <FrameLayout
+                android:id="@+id/hostUrlFrame"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="@dimen/zero"
+                android:background="@color/primary">
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:orientation="vertical">
+
+                    <TextView
+                        android:id="@+id/editText"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:ems="10"
+                        android:text="@string/auth_host_url"
+                        android:textColor="@color/login_text_color" />
+
+                    <com.google.android.material.textfield.TextInputEditText
+                        android:id="@+id/hostUrlInput"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_gravity="bottom"
+                        android:contentDescription="@string/auth_host_address"
+                        android:drawablePadding="@dimen/alternate_half_padding"
+                        android:inputType="textUri"
+                        android:paddingStart="@dimen/zero"
+                        android:paddingLeft="@dimen/zero"
+                        android:paddingEnd="@dimen/alternate_padding_right"
+                        android:paddingRight="@dimen/alternate_padding_right"
+                        android:textColor="@color/login_text_color"
+                        android:textColorHint="@color/login_text_color"
+                        android:theme="@style/Nextcloud.EditText.Login">>
+
+                        <requestFocus />
+
+                    </com.google.android.material.textfield.TextInputEditText>
+
+                </LinearLayout>
+
+                <ImageButton
+                    android:id="@+id/testServerButton"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical|end"
+                    android:layout_marginEnd="@dimen/alternate_half_padding"
+                    android:layout_marginRight="@dimen/alternate_half_padding"
+                    android:background="@android:color/transparent"
+                    android:contentDescription="@string/test_server_button"
+                    android:onClick="onTestServerConnectionClick"
+                    android:padding="@dimen/zero"
+                    android:scaleType="fitCenter"
+                    android:src="@drawable/arrow_right"
+                    android:tint="@color/login_text_color" />
+
+                <ImageButton
+                    android:id="@+id/embeddedRefreshButton"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical|end"
+                    android:layout_marginEnd="@dimen/alternate_half_padding"
+                    android:layout_marginRight="@dimen/alternate_half_padding"
+                    android:background="@android:color/transparent"
+                    android:contentDescription="@string/auth_refresh_button"
+                    android:padding="@dimen/zero"
+                    android:scaleType="fitCenter"
+                    android:src="@drawable/ic_action_refresh"
+                    android:visibility="gone" />
+            </FrameLayout>
+
+            <TextView
+                android:id="@+id/server_status_text"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="@dimen/alternate_margin"
+                android:contentDescription="@string/auth_testing_connection"
+                android:drawableStart="@android:drawable/stat_notify_sync"
+                android:drawableLeft="@android:drawable/stat_notify_sync"
+                android:drawablePadding="@dimen/alternate_half_padding"
+                android:gravity="center_vertical"
+                android:minHeight="@dimen/display_text_min_height"
+                android:text="@string/auth_testing_connection"
+                android:textColor="@color/login_text_color" />
+
+            <com.google.android.material.textfield.TextInputEditText
+                android:id="@+id/account_username"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:autofillHints="username"
+                android:ems="10"
+                android:hint="@string/auth_username"
+                android:inputType="textNoSuggestions"
+                android:textColor="@color/login_text_color"
+                android:textColorHint="@color/login_text_hint_color"
+                android:visibility="gone" />
+
+            <com.google.android.material.textfield.TextInputEditText
+                android:id="@+id/account_password"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:autofillHints="password"
+                android:drawablePadding="@dimen/alternate_half_padding"
+                android:ems="10"
+                android:hint="@string/auth_password"
+                android:inputType="textPassword"
+                android:textColor="@color/login_text_color"
+                android:textColorHint="@color/login_text_hint_color"
+                android:visibility="gone" />
+
+            <TextView
+                android:id="@+id/auth_status_text"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="@dimen/alternate_margin"
+                android:contentDescription="@string/auth_unauthorized"
+                android:drawableStart="@android:drawable/stat_notify_sync"
+                android:drawableLeft="@android:drawable/stat_notify_sync"
+                android:drawablePadding="@dimen/alternate_half_padding"
+                android:gravity="center_vertical"
+                android:text="@string/auth_unauthorized"
+                android:textColor="@color/login_text_color" />
+
+            <ImageButton
+                android:id="@+id/scanQR"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:background="@color/transparent"
+                android:contentDescription="@string/scanQR_description"
+                android:src="@drawable/qrcode_scan"
+                android:theme="@style/Button.Login" />
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/buttonOK"
+                style="@style/Button.Login"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:contentDescription="@string/setup_btn_connect"
+                android:enabled="false"
+                android:padding="@dimen/standard_padding"
+                android:text="@string/setup_btn_connect"
+                android:theme="@style/Button.Login"
+                android:visibility="gone"
+                app:cornerRadius="@dimen/button_corner_radius" />
+        </LinearLayout>
+
+
+    </LinearLayout>
+
+</ScrollView>

+ 82 - 0
src/main/res/values-ar/strings.xml

@@ -3,9 +3,12 @@
     <string name="about_android">%1$s تطبيق آندرويد</string>
     <string name="about_title">حول</string>
     <string name="about_version">الإصدار %1$s</string>
+    <string name="about_version_with_build">إصدار %1$s, بنية #%2$s</string>
     <string name="account_creation_failed">فشل إنشاء الحساب</string>
     <string name="account_icon">أيقونة الحساب </string>
     <string name="account_not_found">الحساب غير موجود </string>
+    <string name="action_clear_failed_uploads">أزل مافشل من المرفوعين</string>
+    <string name="action_empty_notifications">أزل جميع التنبيهات</string>
     <string name="action_empty_trashbin">تفريغ سلة المهملات</string>
     <string name="action_send_share">إرسال/مشاركة</string>
     <string name="action_switch_grid_view">عرض شبكي</string>
@@ -15,6 +18,7 @@
     <string name="actionbar_copy">أنسخ</string>
     <string name="actionbar_mkdir">مجلد جديد</string>
     <string name="actionbar_move">أنقل</string>
+    <string name="actionbar_open_as_richdocument_parameter">فتح بإستخدام %1$s</string>
     <string name="actionbar_open_with">فتح باستخدام</string>
     <string name="actionbar_search">بحث</string>
     <string name="actionbar_see_details">تفاصيل</string>
@@ -24,16 +28,26 @@
     <string name="actionbar_sync">تحديث الحساب</string>
     <string name="active_user">مستخدم نشط</string>
     <string name="activities_no_results_headline">لا توجد أي أنشطة بعدُ</string>
+    <string name="activities_no_results_message">لا أحداث مثل إضافة، تغيير أو مشاركة حتى الآن</string>
     <string name="activity_chooser_send_file_title">أرسل</string>
     <string name="activity_chooser_title">ارسل الرابط الى…</string>
     <string name="activity_icon">النشاط</string>
+    <string name="add_to_cloud">أضفه إلى %1$s</string>
+    <string name="associated_account_not_found">الحساب المرتبط غير موجود!</string>
+    <string name="auth_access_failed">فشل الوصول لـ: %1$s</string>
+    <string name="auth_account_does_not_exist">هذا الحساب لم تتم إضافته في هذا الجهاز بعد</string>
+    <string name="auth_account_not_new">حسابٌ لنفس المستخدم والخادم موجود حالياً على هذا الجهاز</string>
     <string name="auth_account_not_the_same">المستخدم المدخل لا يتوافق مع المستخدم الموجود في الحساب </string>
     <string name="auth_bad_oc_version_title">إصدار الخادم غير معروف</string>
+    <string name="auth_can_not_auth_against_server">لايمكن إثبات الهوية مع هذا الخادم</string>
     <string name="auth_check_server">فحص الخادم</string>
     <string name="auth_connection_established">تم الاتصال</string>
     <string name="auth_expired_basic_auth_toast">إدخل كلمة المرور الخاصة بك من فضلك</string>
+    <string name="auth_fail_get_user_name">خادمك لا يرد بإسم مستخدم صحيح، الرجاء بالإتصال بمدير الخادم</string>
     <string name="auth_host_address">عنوان الخادم</string>
     <string name="auth_host_url">عنوان الخادوم https://…</string>
+    <string name="auth_illegal_login_used">رابط الدخول غير شرعي</string>
+    <string name="auth_incorrect_address_title">صيغة عنوانٌ خطأ للخادم</string>
     <string name="auth_incorrect_path_title">لم يتم العثور على الخادم</string>
     <string name="auth_no_net_conn_title">لا يتوفر اتصال</string>
     <string name="auth_nossl_plain_ok_title">الاتصال الآمن غير متاح</string>
@@ -41,27 +55,53 @@
     <string name="auth_oauth_error">فشل في التحقق</string>
     <string name="auth_oauth_error_access_denied">تم رفض الوصول من قبل الخادم المرخص</string>
     <string name="auth_password">كلمة السر</string>
+    <string name="auth_redirect_non_secure_connection_title">إتصالٌ آمن نُقِلَ خلال معبر غير آمن</string>
     <string name="auth_refresh_button">إنعاش الاتصال</string>
     <string name="auth_secure_connection">نجح الاتصال الآمن</string>
     <string name="auth_ssl_general_error_title">فشل في تهيئة SSL</string>
+    <string name="auth_ssl_unverified_server_title">لا يمكن التأكد من هوية الخادم الأمنية SSL</string>
     <string name="auth_testing_connection">تجريب الإتصال</string>
     <string name="auth_timeout_title">الخادم أخذ كثيرا من الوقت للرد</string>
     <string name="auth_trying_to_login">محاولة الدخول…</string>
     <string name="auth_unauthorized">اسم المستخدم أو كلمة المرور خاطئة</string>
+    <string name="auth_unknown_error_exception_title">خطأ غير معروف: %1$s</string>
+    <string name="auth_unknown_error_http_title">خطأ غير معروف حدث في بروتوكول HTTP!</string>
     <string name="auth_unknown_error_title">حدث خطأ غير معروف !</string>
     <string name="auth_unknown_host_title">لم يتم العثور على المضيف</string>
     <string name="auth_unsupported_multiaccount">%1$s لا يدعم الحسابات المتعددة </string>
     <string name="auth_username">إسم المستخدم</string>
+    <string name="auth_wrong_connection_title">لا يمكن إنشاء إتصال</string>
+    <string name="auth_wtf_reenter_URL">حالة غير متوقعة، الرجاء إدخال عنوان الخادم مرة أخرى</string>
+    <string name="authentication_exception">استثناء في المصادقة</string>
+    <string name="auto_upload_file_behaviour_kept_in_folder">ترك في مجلد الأصل، بسبب كونه للقرائة فقط</string>
+    <string name="auto_upload_on_wifi">قم بالرفع عبر شبكة لاسلكية غير محدودة البيانات فقط</string>
     <string name="auto_upload_path">/رفع تلقائي</string>
+    <string name="autoupload_create_new_custom_folder">أنشئ إعداد مجلد خاص جديد</string>
+    <string name="autoupload_custom_folder">إعداد مجلد خاص</string>
+    <string name="autoupload_disable_power_save_check">تعطيل التحقق من حفظ الطاقة</string>
     <string name="avatar">الصورة الرمزية</string>
     <string name="battery_optimization_close">إغلاق</string>
     <string name="battery_optimization_disable">تعطيل</string>
+    <string name="battery_optimization_message">قد يكون حفظ البطارية مفعلاً في جهازك. لذلك قد لا يعمل الرفع التلقائي إلا إذا سمحت لهذا التطبيق من أن يستبعد من هذه الميزة.</string>
+    <string name="battery_optimization_no_setting">لا يمكن فتح إعدادات البطارية مباشرة من هنا. نرجو أن تقوم بتعديل الإعدادات يدوياً.</string>
+    <string name="battery_optimization_title">تحسين البطارية</string>
+    <string name="certificate_load_problem">هناك مشكلة في تحميل الشهادة.</string>
+    <string name="changelog_dev_version">المتغيرات في تطبيق المطورين</string>
+    <string name="checkbox">مربع</string>
+    <string name="choose_local_folder">إختر المجلد المحلي...</string>
+    <string name="choose_remote_folder">إختر مجلد عن بُعد...</string>
+    <string name="clear_notifications_failed">فشلت في إزالة التنبيهات.</string>
+    <string name="clipboard_label">نُسِخ النص من %1$s</string>
+    <string name="clipboard_no_text_to_copy">لم يستلم نص لنسخه في الحافظة</string>
+    <string name="clipboard_text_copied">نُسِخ الرابط</string>
+    <string name="clipboard_unexpected_error">خطأٌ غير متوقع أثناء النسخ إلى الحافظة</string>
     <string name="common_back">رجوع</string>
     <string name="common_cancel">إلغاء</string>
     <string name="common_cancel_sync">ألغ المزامنة</string>
     <string name="common_choose_account">اختر حسابا</string>
     <string name="common_delete">حذف</string>
     <string name="common_error">خطأ</string>
+    <string name="common_error_out_memory">لا يوجد ذاكرة عشوائية كافية</string>
     <string name="common_error_unknown">خطأ غير معروف. </string>
     <string name="common_loading">تحميل…</string>
     <string name="common_no">لا</string>
@@ -75,26 +115,68 @@
     <string name="common_switch_account">تبديل الحساب</string>
     <string name="common_unknown">غير معروف</string>
     <string name="common_yes">نعم</string>
+    <string name="community_beta_headline">جَرِّب إصدار المطوٌرين</string>
+    <string name="community_beta_text">يشمل هذا جميع المميزات الحديثة ويعتبر طليعة الإصدارات. الأخطاء متوَّقعة، لذا إذا حصلت أخطاء، نرجو منك الإبلاغ عنهم.</string>
+    <string name="community_contribute_forum_forum">المنتدى</string>
+    <string name="community_contribute_forum_text">قم بمساعدة الآخرين على</string>
+    <string name="community_contribute_github_text">رَاجِع، أَصّلِح وبَّرمج، إنظر إلى %1$s لمزيد من التفاصيل</string>
+    <string name="community_contribute_headline">ساهم بفعالية</string>
+    <string name="community_contribute_irc_text">إنضم إلى الدردشة على IRC:</string>
+    <string name="community_contribute_translate_text">التطبيق</string>
+    <string name="community_contribute_translate_translate">تَرجِم</string>
+    <string name="community_dev_direct_download">حمل نسخة المطوريين مباشرة</string>
+    <string name="community_dev_fdroid">إحصل على نسخة المطوريين من تطبيق إف-درويد F-Droid</string>
+    <string name="community_rc_fdroid">إحصل على النسخة المرشحة من تطبيق إف-درويد F-Droid</string>
+    <string name="community_rc_play_store">إحصل على النسخة المرشحة من متجر جوجل بلاي Google Play</string>
+    <string name="community_release_candidate_headline">النسخة المرشحة</string>
+    <string name="community_release_candidate_text">النسخة المرشحة (RC) هي نسخة متوقعة قريبا وتعتبر شبه ثابته. تجربتك لها قد تساعد في التأكد من ذلك. قم بالتسجيل من خلال متجر جوجل بلاي أو يدويا من خلال إختيار الإصدار في تطبيق إف-درويد F-Droid</string>
+    <string name="community_testing_bug_text">وجَدّت خطأ برمجي؟ شِّيءٌ نَّاقص؟</string>
+    <string name="community_testing_headline">ساعدنا بالتجريب</string>
+    <string name="community_testing_report_text">إرسل تقرير أخطاء على جيت هب GitHub</string>
+    <string name="community_testing_version_text">هل أنت راغب في المساعدة بتجريب النسخة القادمة؟</string>
     <string name="configure_new_media_folder_detection_notifications">إعداد</string>
+    <string name="confirmation_remove_file_alert">هل توَدُّ حقاً حذف %1$s؟</string>
+    <string name="confirmation_remove_files_alert">هل توَدُّ حقاً حذف العناصر المختارة؟</string>
+    <string name="confirmation_remove_folder_alert">هل توَدُّ حقاً حذف %1$s وما يحتويه؟</string>
+    <string name="confirmation_remove_folders_alert">هل توَدُّ حقاً حذف العناصر المختارة وما يحتوّه؟</string>
     <string name="confirmation_remove_local">محليا فقط</string>
     <string name="conflict_keep_both">الاحتفاظ بالنسختين</string>
+    <string name="conflict_message">أي ملفات تود أن تحفظها؟ عند إختيارك للنسختين من الملفات، سيضاف رقم في آخر إسم النسخة المحلية.</string>
+    <string name="conflict_title">تعارُض الملف</string>
     <string name="conflict_use_local_version">النسخة المحلية</string>
     <string name="conflict_use_server_version">نسخة السيرفر</string>
+    <string name="contaclist_restore_selected">إسترجع جهات الإتصال المختارة</string>
     <string name="contactlist_account_chooser_title">إختر الحساب الذي تود استرجاعه</string>
     <string name="contactlist_item_icon">أيقونة المستخدمين المتواجدين في قائمة جهات الإتصال</string>
+    <string name="contactlist_no_permission">لا يوجد إذن، لم يتم إستيراد أي شيء.</string>
     <string name="contacts_automatic_backup">نسخ احتياطي تلقائي</string>
     <string name="contacts_backup_button">نسخة احتياطية الأن</string>
     <string name="contacts_last_backup">أخر نسخة احتياطية</string>
     <string name="contacts_preference_backup_never">بتاتاً</string>
     <string name="contacts_preference_choose_date">أختر التاريخ</string>
+    <string name="contacts_preferences_backup_scheduled">النسخ الإحتياطي دُرِجَ في الجدول وسيتم البدء قريباً</string>
+    <string name="contacts_preferences_import_scheduled">الإستيراد دُرِجَ في الجدول وسيتم البدء قريباً</string>
     <string name="contacts_preferences_no_file_found">لم يتم ايجاد أي ملف</string>
+    <string name="contacts_preferences_something_strange_happened">لم يتم العثور على نسختك الإحتياطية الأخيرة!</string>
+    <string name="contacts_read_permission">بحاجة لأُذن قرائة جهات الإتصال</string>
     <string name="copied_to_clipboard">تم النسخ إلى الحافظة</string>
+    <string name="copy_file_error">حدث خطأٌ خلال محاولة نسخ هذا الملف أو المجلد</string>
+    <string name="copy_file_invalid_into_descendent">لا يمكن نسخ مجلد في مجلدات هوَّ يتضمنها</string>
+    <string name="copy_file_invalid_overwrite">هذا الملف موجود حالياً في المجلد المُّسّتَقبِل</string>
+    <string name="copy_file_not_found">غير قادر على النسخ. يرجى التحقق من وجود الملف</string>
     <string name="copy_internal_link">انسخ الرابط الداخلي</string>
+    <string name="copy_internal_link_subline">يعمل فقط للمستخدمين الذين لديهم حق الوصول إلى هذا المجلد</string>
     <string name="copy_link">نسخ الرابط</string>
+    <string name="copy_move_to_encrypted_folder_not_supported">النسخ أو النقل إلى مجلد مشفر حالياً غير مدعوم.</string>
     <string name="copy_to">انسخه إلى…</string>
+    <string name="could_not_download_image">لايمكن تحميل الصورة بأكملها</string>
+    <string name="could_not_retrieve_url">لايمكن إسترجاع العنوان</string>
     <string name="create_dir_fail_msg">تعذر إنشاء المُجلّد</string>
+    <string name="create_file_from_template">إنشاء ملف من قالب...</string>
     <string name="create_new_document">أنشئ مستندا جديدا</string>
     <string name="create_new_folder">أنشئ مجلدا جديدا</string>
+    <string name="create_new_presentation">إنشئ عرض تقديمي جديد</string>
+    <string name="create_new_spreadsheet">أنشئ جدول بيانات جديد</string>
     <string name="date_unknown">مجهول</string>
     <string name="delete_account">حذف الحساب</string>
     <string name="deselect_all">إلغاء تحديد الكل</string>

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

@@ -266,6 +266,7 @@
     <string name="favorite_icon">Preferits</string>
     <string name="feedback_no_mail_app">No hi ha cap aplicació disponible per enviar correus electrònics!</string>
     <string name="file_delete">Suprimeix</string>
+    <string name="file_detail_activity_error">Hi ha hagut un error mentre es recuperaven les activitats per al fitxer</string>
     <string name="file_details_no_content">No s\'han pogut carregar els detalls</string>
     <string name="file_icon">Fitxer</string>
     <string name="file_keep">Mantén</string>
@@ -419,6 +420,7 @@
     <string name="new_notification">Nova notificació</string>
     <string name="new_version_was_created">S\'ha creat una nova versió</string>
     <string name="no_browser_available">No hi ha cap aplicació disponible per manegar els enllaços</string>
+    <string name="no_mutliple_accounts_allowed">Només es permet un compte.</string>
     <string name="no_pdf_app_available">No hi ha cap aplicació disponible per manegar els PDF</string>
     <string name="note_confirm">Envia</string>
     <string name="note_could_not_sent">No s\'ha pogut enviar la nota</string>

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

@@ -266,6 +266,7 @@
     <string name="favorite_icon">Favorit</string>
     <string name="feedback_no_mail_app">Ingen app tilgængelig til afsendelse af mails!</string>
     <string name="file_delete">Slet</string>
+    <string name="file_detail_activity_error">Fejl ved indlæsning af aktiviteter for fil</string>
     <string name="file_details_no_content">Fejl ved indlæsning af detaljer</string>
     <string name="file_icon">Fil</string>
     <string name="file_keep">Behold</string>
@@ -419,6 +420,7 @@
     <string name="new_notification">Ny Notifikation</string>
     <string name="new_version_was_created">Ny version oprettet</string>
     <string name="no_browser_available">Ingen App tilgængelig til håndtering af links</string>
+    <string name="no_mutliple_accounts_allowed">kun en konto tilladt</string>
     <string name="no_pdf_app_available">Ingen App tilgængelig til håndtering af PDF</string>
     <string name="note_confirm">Send</string>
     <string name="note_could_not_sent">Kunne ikke sende note</string>

+ 3 - 0
src/main/res/values-de/strings.xml

@@ -266,6 +266,7 @@
     <string name="favorite_icon">Favoriten</string>
     <string name="feedback_no_mail_app">Keine App verfügbar für den E-Mailversand!</string>
     <string name="file_delete">Löschen</string>
+    <string name="file_detail_activity_error">Fehler beim Abrufen der Aktivitäten für die Datei</string>
     <string name="file_details_no_content">Fehler beim Laden der Details</string>
     <string name="file_icon">Datei</string>
     <string name="file_keep">Behalten</string>
@@ -419,6 +420,7 @@
     <string name="new_notification">Neue Benachrichtigung</string>
     <string name="new_version_was_created">Neue Version erstellt</string>
     <string name="no_browser_available">Keine App verfügbar um Links zu öffnen</string>
+    <string name="no_mutliple_accounts_allowed">Nur ein Konto zulässig</string>
     <string name="no_pdf_app_available">Keine App verfügbar um PDFs anzuzeigen</string>
     <string name="note_confirm">Senden</string>
     <string name="note_could_not_sent">Notiz konnte nicht versandt werden</string>
@@ -536,6 +538,7 @@
     <string name="recommend_subject">Probieren Sie %1$s auf Ihrem Smartphone</string>
     <string name="recommend_text">Ich empfehle Ihnen %1$s auf Ihrem Smartphone zu nutzen.\nHerunterladen: %2$s</string>
     <string name="recommend_urls">%1$s oder %2$s</string>
+    <string name="remote_file_fetch_failed">Datei nicht gefunden!</string>
     <string name="remove_fail_msg">Löschung fehlgeschlagen</string>
     <string name="remove_notification_failed">Entfernen der Benachrichtigungen fehlgeschlagen.</string>
     <string name="remove_push_notification">Entfernen</string>

+ 37 - 0
src/main/res/values-el/strings.xml

@@ -33,6 +33,7 @@
     <string name="activity_chooser_title">Αποστολή συνδέσμου σε…</string>
     <string name="activity_icon">Δραστηριότητα</string>
     <string name="add_to_cloud">Προσθήκη στο %1$s</string>
+    <string name="associated_account_not_found">Δεν βρέθηκε ο συνδεδεμένος λογαριασμός!</string>
     <string name="auth_access_failed">Αποτυχία πρόσβασης: %1$s</string>
     <string name="auth_account_does_not_exist">Ο λογαριασμός δεν υπάρχει ακόμα στη συσκευή</string>
     <string name="auth_account_not_new">Ένας λογαριασμός για τον ίδιο χρήστη και διακομιστή υπάρχει ήδη στη συσκευή</string>
@@ -72,10 +73,12 @@
     <string name="auth_wrong_connection_title">Αδυναμία δημιουργίας σύνδεσης</string>
     <string name="auth_wtf_reenter_URL">Απροσδόκητη κατάσταση, παρακαλoύμε εισάγετε ξανά τη διεύθυνση του διακομιστή</string>
     <string name="authentication_exception">Παρατυπία Πιστοποίησης</string>
+    <string name="auto_upload_file_behaviour_kept_in_folder">διατηρηθεί στον αρχικό φάκελο, όπως είναι μόνο για ανάγνωση</string>
     <string name="auto_upload_on_wifi">Μεταφόρτωση μόνο μέσω wifi</string>
     <string name="auto_upload_path">/ΑυτόματηΜεταφόρτωση</string>
     <string name="autoupload_create_new_custom_folder">Δημιουργήστε νέα διαμόρφωση προσαρμοσμένου φακέλου</string>
     <string name="autoupload_custom_folder">Διαμορφώστε έναν προσαρμοσμένο φάκελο</string>
+    <string name="autoupload_disable_power_save_check">Απενεργοποίηση ελέγχου εξοικονόμησης ενέργειας</string>
     <string name="avatar">Εικόνα προφίλ</string>
     <string name="battery_optimization_close">Κλείσιμο</string>
     <string name="battery_optimization_disable">Απενεργοποίηση</string>
@@ -83,6 +86,7 @@
     <string name="battery_optimization_no_setting">Αδυναμία απευθείας έναρξης των ρυθμίσεων της μπαταρίας. Παρακαλούμε προσαρμόστε χειροκίνητα τις ρυθμίσεις.</string>
     <string name="battery_optimization_title">Βελτιστοποίηση μπαταρίας</string>
     <string name="certificate_load_problem">Πρόβλημα φόρτωσης του πιστοποιητικού.</string>
+    <string name="changelog_dev_version">Αρχείο αλλαγών της έκδοσης προγραμματιστή</string>
     <string name="checkbox">Πλαίσιο ελέγχου</string>
     <string name="choose_local_folder">Επιλέξτε τοπικό φάκελο…</string>
     <string name="choose_remote_folder">Επιλέξτε απομακρυσμένο φάκελο…</string>
@@ -112,11 +116,23 @@
     <string name="common_unknown">άγνωστο</string>
     <string name="common_yes">Ναι</string>
     <string name="community_beta_headline">Δοκιμή της έκδοσης προγραμματιστή</string>
+    <string name="community_beta_text">Αυτή περιέχει όλες τις επερχόμενες λειτουργίες και δαθέτει ότι τελευταίο έχει ενσωματωθεί. Εάν και εφόσον προκύψουν σφάλματα, σας παρακαλούμε να μας τα αναφέρετε.</string>
+    <string name="community_contribute_forum_forum">Φόρουμ</string>
     <string name="community_contribute_forum_text">Βοήθησε άλλους στο</string>
     <string name="community_contribute_github_text">Εξέτασε, τροποποίησε και γράψε κώδικα, δες το %1$s για πληροφορίες</string>
     <string name="community_contribute_headline">Συνέβαλε ενεργά</string>
+    <string name="community_contribute_irc_text">Μπείτε για chat στο IRC:</string>
+    <string name="community_contribute_translate_text">η εφαρμογή</string>
+    <string name="community_contribute_translate_translate">Μετάφραση</string>
+    <string name="community_dev_direct_download">Απευθείας λήψη της υπό ανάπτυξη έκδοσης</string>
+    <string name="community_dev_fdroid">Λάβετε την υπό ανάπτυξη έκδοση από την εφαρμογή F-Droid</string>
+    <string name="community_rc_fdroid">Λάβετε την υπό ανάπτυξη έκδοση από την εφαρμογή F-Droid</string>
+    <string name="community_rc_play_store">Λάβετε την υπό ανάπτυξη έκδοση από την εφαρμογή Google Play store</string>
+    <string name="community_release_candidate_headline">Release candidate</string>
+    <string name="community_testing_bug_text">Βρήκατε σφάλμα; Κάτι σας φαίνεται παράξενο;</string>
     <string name="community_testing_headline">Βοήθησε δοκιμάζοντας</string>
     <string name="community_testing_report_text">Ανέφερε ένα ζήτημα στο GitHub</string>
+    <string name="community_testing_version_text">Σας ενδιαφέρει να μας βοηθήσετε να δοκιμάσουμε την επόμενη έκδοση;</string>
     <string name="configure_new_media_folder_detection_notifications">Ρύθμιση</string>
     <string name="confirmation_remove_file_alert">Θέλετε σίγουρα να διαγράψετε το %1$s;</string>
     <string name="confirmation_remove_files_alert">Θέλετε να διαγράψετε τα επιλεγμένα αντικείμενα;</string>
@@ -152,6 +168,7 @@
     <string name="copy_link">Αντιγραφή συνδέσμου</string>
     <string name="copy_move_to_encrypted_folder_not_supported">Η αντιγραφή/μετακίνηση σε κρυπτογραφημένο φάκελο δεν υποστηρίζεται αυτή τη στιγμή</string>
     <string name="copy_to">Αντιγραφή σε…</string>
+    <string name="could_not_download_image">Αδυναμία λήψης της πλήρους εικόνας</string>
     <string name="could_not_retrieve_url">Αδυναμία ανάκτησης url</string>
     <string name="create_dir_fail_msg">Αδυναμία δημιουργίας φακέλου</string>
     <string name="create_file_from_template">Δημιουργία αρχείου από πρότυπο...</string>
@@ -243,6 +260,7 @@
     <string name="favorite_icon">Αγαπημένο</string>
     <string name="feedback_no_mail_app">Δεν υπάρχει διαθέσιμη εφαρμογή για αποστολή αλληλογραφίας!</string>
     <string name="file_delete">Διαγραφή</string>
+    <string name="file_detail_activity_error">Σφάλμα ανάκτησης δραστηριοτήτων για το αρχείο</string>
     <string name="file_details_no_content">Αποτυχία φόρτωσης λεπτομερειών</string>
     <string name="file_icon">Αρχείο</string>
     <string name="file_keep">Διατήρηση</string>
@@ -307,6 +325,9 @@
     <string name="filename_hint">Όνομα αρχείου</string>
     <string name="files_drop_not_supported">Αυτό είναι χαρακτηριστικό του Nextcloud, παρακαλούμε αναβαθμίστε.</string>
     <string name="first_run_1_text">Κρατήστε τα δεδομένα σας ασφαλή και υπό τον έλεγχό σας</string>
+    <string name="first_run_2_text">Ασφαλής συνεργασία &amp; ανταλλαγή αρχείων</string>
+    <string name="first_run_3_text">Εύχρηστο web mail, ημερολόγιο &amp; επαφές</string>
+    <string name="first_run_4_text">Κοινή χρήση οθόνης, ηλεκτρονικές συναντήσεις &amp; συνέδρια</string>
     <string name="folder_already_exists">Ο φάκελος υπάρχει ήδη</string>
     <string name="folder_confirm_create">Δημιουργία</string>
     <string name="folder_icon">Εικονίδιο φακέλου</string>
@@ -348,6 +369,7 @@
     <string name="logs_menu_search">Αναζήτηση καταγραφών</string>
     <string name="logs_menu_send">Αποστολή καταγραφών μέσω e-mail</string>
     <string name="logs_status_loading">Φορτώνει...</string>
+    <string name="logs_status_not_filtered">Αρχεία καταγραφής: %1$d kb, χωρίς φίλτρο</string>
     <string name="logs_title">Καταγραφές</string>
     <string name="maintenance_mode">Ο διακομιστής είναι σε κατάσταση συντήρησης</string>
     <string name="manage_space_clear_data">Εκκαθάριση δεδομένων</string>
@@ -385,11 +407,13 @@
     <string name="network_error_socket_timeout_exception">Παρουσιάστηκε σφάλμα κατά την αναμονή για τον διακομιστή. Η λειτουργία δεν μπορεί να ολοκληρωθεί</string>
     <string name="network_host_not_available">Η λειτουργία δεν μπορεί να ολοκληρωθεί. Ο διακομιστής δεν είναι διαθέσιμος</string>
     <string name="new_comment">Νέο σχόλιο…</string>
+    <string name="new_media_folder_detected">Εντοπίστηκε νέος φάκελος πολυμέσων %1$s.</string>
     <string name="new_media_folder_photos">φωτογραφία</string>
     <string name="new_media_folder_videos">Βίντεο</string>
     <string name="new_notification">Νέα ειδοποίηση</string>
     <string name="new_version_was_created">Νέα έκδοση δημιουργήθηκε</string>
     <string name="no_browser_available">Δεν υπάρχει διαθέσιμη εφαρμογή για χειρισμό συνδέσμων</string>
+    <string name="no_mutliple_accounts_allowed">Μόνο ένας λογαριασμός επιτρέπεται</string>
     <string name="no_pdf_app_available">Δεν υπάρχει διαθέσιμη εφαρμογή για χειρισμό PDF</string>
     <string name="note_confirm">Αποστολή</string>
     <string name="note_could_not_sent">Αδυναμία αποστολής σημείωσης</string>
@@ -400,6 +424,7 @@
     <string name="notification_channel_file_observer_name">Παρατηρητής αρχείων</string>
     <string name="notification_channel_file_sync_description">Δείχνει την πρόοδο και το αποτέλεσμα συγχρονισμού του αρχείου</string>
     <string name="notification_channel_file_sync_name">Συγχρονισμός αρχείου</string>
+    <string name="notification_channel_general_description">Προβολή ειδοποι΄σεων για νέους φακέλους πολυμέσων και άλλα παρόμοια</string>
     <string name="notification_channel_general_name">Γενικές ειδοποιήσεις</string>
     <string name="notification_channel_media_description">Πρόοδος αναπαραγωγής μουσικής</string>
     <string name="notification_channel_media_name">Αναπαραγωγή πολυμέσων</string>
@@ -425,6 +450,8 @@
     <string name="participate_contribute_translate_translate">Μετάφραση</string>
     <string name="participate_dev_direct_download">Απευθείας λήψη της υπό ανάπτυξη έκδοσης</string>
     <string name="participate_dev_fdroid">Λάβετε την υπό ανάπτυξη έκδοση από την εφαρμογή F-Droid</string>
+    <string name="participate_rc_fdroid">Λάβετε την υπό ανάπτυξη έκδοση από την εφαρμογή F-Droid</string>
+    <string name="participate_rc_play_store">Λάβετε την υπό ανάπτυξη έκδοση από την εφαρμογή Google Play store</string>
     <string name="participate_release_candidate_headline">Release candidate</string>
     <string name="participate_release_candidate_text">Η υποψήφια έκδοση (RC) είναι ένα στιγμιότυπο της επερχόμενης έκδοσης και να αναμένεται να είναι σταθερή. Δοκιμάζοντας τις προσωπικές σας ρυθμίσεις μπορεί να βοηθήσει στην διασφάλιση της έκδοσης. Δηλώστε υποψηφιότητα δοκιμής στο Play store ή αναζητήστε χειροκίνητα στον τομέα \"εκδόσεων\" στο F-Droid.</string>
     <string name="participate_testing_bug_text">Βρήκατε σφάλμα; Κάτι σας φαίνεται παράξενο;</string>
@@ -443,11 +470,13 @@
     <string name="permission_allow">Επιτρέπεται</string>
     <string name="permission_deny">Απόρριψη</string>
     <string name="permission_storage_access">Επιπλέον διακαιώματα απαιτούνται για μεταφόρτωση και λήψη αρχείων.</string>
+    <string name="picture_set_as_no_app">Δεν βρέθηκε εφαρμογή για τον ορισμό φωτογραφίας</string>
     <string name="placeholder_fileSize">389 KB</string>
     <string name="placeholder_filename">placeholder.txt</string>
     <string name="placeholder_media_time">12:23:45</string>
     <string name="placeholder_sentence">Αυτό είναι ένα σημείο placeholder</string>
     <string name="placeholder_timestamp">2012/05/18 12:23 PM</string>
+    <string name="power_save_check_dialog_message">Η απενεργοποίηση του έλεγχου εξοικονόμησης ενέργειας ίσως έχει ως αποτέλεσμα το ανέβασμα αρχείων ενώ βρίσκεστε σε κατάσταση χαμηλής μπαταρίας</string>
     <string name="pref_behaviour_entries_delete_file">διαγραφεί</string>
     <string name="pref_behaviour_entries_keep_file">διατηρηθεί στον αρχικό φάκελο</string>
     <string name="pref_behaviour_entries_move">μετακινηθεί στον φάκελο εφαρμογών</string>
@@ -471,9 +500,11 @@
     <string name="prefs_instant_upload_path_use_subfolders_summary">Αποθήκευση σε υποφακέλους με βάση το χρόνο και μήνα</string>
     <string name="prefs_instant_upload_path_use_subfolders_title">Χρήση υποφακέλων</string>
     <string name="prefs_license">Άδεια χρήσης</string>
+    <string name="prefs_lock">Κωδικός πρόσβασης εφαρμογής</string>
     <string name="prefs_lock_device_credentials_enabled">Ενεργοποιημένα διαπιστευτήρια συσκευής</string>
     <string name="prefs_lock_device_credentials_not_setup">Δεν έχουν ρυθμιστεί διαπιστευτήρια συσκευής</string>
     <string name="prefs_lock_none">Τίποτα</string>
+    <string name="prefs_lock_title">Προστασία εφαρμογής με τη χρήση</string>
     <string name="prefs_lock_using_device_credentials">Διαπιστευτήρια συσκευής</string>
     <string name="prefs_lock_using_passcode">Κωδικός πρόσβασης</string>
     <string name="prefs_manage_accounts">Διαχείριση λογαριασμών</string>
@@ -515,6 +546,11 @@
     <string name="screenshot_01_gridView_heading">Προστασία των δεδομένων σας</string>
     <string name="screenshot_02_listView_heading">Περιήγηση και διαμοιρασμός</string>
     <string name="screenshot_04_accounts_heading">Όλοι οι λογαριασμοί σας</string>
+    <string name="screenshot_04_accounts_subline">σε ένα σημείο</string>
+    <string name="screenshot_05_autoUpload_heading">Αυτόματη μεταφόρτωση</string>
+    <string name="screenshot_05_autoUpload_subline">για τις φωτογραφίες &amp; τα βίντεό σας</string>
+    <string name="screenshot_06_davdroid_heading">Συγχρονισμός ημερολογίου &amp; επαφών</string>
+    <string name="screenshot_06_davdroid_subline">με DAVx5 (πρώην DAVdroid)</string>
     <string name="search_users_and_groups_hint">Αναζήτηση χρηστών και ομάδων</string>
     <string name="select_all">Επιλογή όλων</string>
     <string name="select_template">Επιλογή προτύπου</string>
@@ -612,6 +648,7 @@
     <string name="storage_movies">Βίντεο</string>
     <string name="storage_music">Μουσική</string>
     <string name="storage_pictures">Φωτογραφίες</string>
+    <string name="stream">Ροή με...</string>
     <string name="stream_not_possible_message">Παρακαλούμε να γίνει λήψη των μέσων ή να χρησιμοποιηθεί εξωτερική εφαρμογή</string>
     <string name="subject_shared_with_you">Ο \"%1$s\" διαμοιράστηκε μαζί σας</string>
     <string name="subject_user_shared_with_you">Ο %1$s διαμοιράστηκε το \"%2$s\" με εσάς</string>

+ 3 - 0
src/main/res/values-es/strings.xml

@@ -266,6 +266,7 @@
     <string name="favorite_icon">Favorito</string>
     <string name="feedback_no_mail_app">¡No hay ninguna app disponible para mandar correos!</string>
     <string name="file_delete">Eliminar</string>
+    <string name="file_detail_activity_error">Error al recuperar las actividades del archivo</string>
     <string name="file_details_no_content">Fallo al cargar los detalles</string>
     <string name="file_icon">Archivo</string>
     <string name="file_keep">Mantener</string>
@@ -419,6 +420,7 @@
     <string name="new_notification">Nueve notificación</string>
     <string name="new_version_was_created">Se ha creado una versión nueva</string>
     <string name="no_browser_available">No hay ninguna app disponible para manejar enlaces</string>
+    <string name="no_mutliple_accounts_allowed">Sólo se permite una cuenta</string>
     <string name="no_pdf_app_available">No hay apps disponibles para manejar PDF</string>
     <string name="note_confirm">Enviar</string>
     <string name="note_could_not_sent">No se puede enviar la nota</string>
@@ -536,6 +538,7 @@
     <string name="recommend_subject">¡Prueba %1$s en tu móvil!</string>
     <string name="recommend_text">Quiero invitarte a usar %1$s en tu móvil.\nDescárgalo aquí: %2$s</string>
     <string name="recommend_urls">%1$s o %2$s</string>
+    <string name="remote_file_fetch_failed">¡No se pudo encontrar el archivo!</string>
     <string name="remove_fail_msg">Ha fallado el borrado</string>
     <string name="remove_notification_failed">Fallo al quitar notificación.</string>
     <string name="remove_push_notification">Quitar</string>

+ 4 - 0
src/main/res/values-eu/strings.xml

@@ -3,6 +3,7 @@
     <string name="about_android">%1$s Android aplikazioa</string>
     <string name="about_title">Honi buruz</string>
     <string name="about_version">%1$s bertsioa</string>
+    <string name="about_version_with_build">bertsioa: %1$s, konpilazioa: #%2$s</string>
     <string name="account_creation_failed">Kontuaren sorrerak huts egin du</string>
     <string name="account_icon">Kontuaren ikonoa</string>
     <string name="account_not_found">Ez da kontua aurkitu!</string>
@@ -199,6 +200,7 @@
     <string name="downloader_download_succeeded_ticker">Jaitsita</string>
     <string name="downloader_not_downloaded_yet">Oraindik deskargatu gabe</string>
     <string name="drawer_close">Alboko barra itxi</string>
+    <string name="drawer_community">Komunitatea</string>
     <string name="drawer_current_account">Uneko kontua</string>
     <string name="drawer_end_account">Azken kontua</string>
     <string name="drawer_header_background">Goiburuko kutxaren atzeko planoaren irudia</string>
@@ -255,6 +257,7 @@
     <string name="etm_title">Ingeniaritzako froga modua </string>
     <string name="fab_label">Gehitu edo igo</string>
     <string name="failed_to_download">Huts egin du fitxategia deskarga kudeatzailera pasatzean</string>
+    <string name="failed_to_print">Huts egin du fitxategia inprimatzean</string>
     <string name="fallback_weblogin_back">Atzera</string>
     <string name="fallback_weblogin_text">Lehengoratu saioa hasteko metodo zaharrera</string>
     <string name="favorite">Gogokoenetara gehitu</string>
@@ -722,6 +725,7 @@
     <string name="upload_list_empty_text_auto_upload">Edukiak igo edo auto-igotzea aktiba ezazu.</string>
     <string name="upload_local_storage_full">Biltegiratze lokala beteta</string>
     <string name="upload_local_storage_not_copied">Ezin izan da fitxategia kopiatu biltegiratze lokalera</string>
+    <string name="upload_lock_failed">Karpeta blokeatzeak huts egin du</string>
     <string name="upload_query_move_foreign_files">Hautatutako fitxategiak ezin dira %1$sra kopiatu ez dagoelako toki nahikorik. Kopiatu ordez bertara mugitu nahi dituzu?</string>
     <string name="upload_sync_conflict">Sinkronizazio gatazka, ebatzi eskuz</string>
     <string name="upload_unknown_error">Akats ezezaguna</string>

+ 4 - 0
src/main/res/values-fi-rFI/strings.xml

@@ -169,6 +169,7 @@
     <string name="copy_link">Kopioi linkki</string>
     <string name="copy_move_to_encrypted_folder_not_supported">Kopiointi/siirto salattuun kansioon ei ole mahdollista.</string>
     <string name="copy_to">Kopio kohteeseen…</string>
+    <string name="could_not_download_image">Koko kuvan lataaminen epäonnistui</string>
     <string name="could_not_retrieve_url">URL:n nouto epännistui</string>
     <string name="create_dir_fail_msg">Kansion luominen ei onnistunut</string>
     <string name="create_file_from_template">Luodaan tiedosto mallipohjasta…</string>
@@ -201,6 +202,7 @@
     <string name="downloader_download_succeeded_ticker">Ladattu</string>
     <string name="downloader_not_downloaded_yet">Ei vielä ladattu</string>
     <string name="drawer_close">Sulje valikko</string>
+    <string name="drawer_community">Yhteisö</string>
     <string name="drawer_current_account">Nykyinen tili</string>
     <string name="drawer_end_account">Viimeisin tili</string>
     <string name="drawer_header_background">Taustakuva \"Drawer\":in yläosassa</string>
@@ -256,6 +258,7 @@
     <string name="etm_preferences">Asetukset</string>
     <string name="etm_title">Kehittäjän testitila</string>
     <string name="fab_label">Lisää tai lähetä</string>
+    <string name="failed_to_print">Tiedoston tulostaminen epäonnistui</string>
     <string name="fallback_weblogin_back">Takaisin</string>
     <string name="fallback_weblogin_text">Palaa vanhaan kirjautumistapaan</string>
     <string name="favorite">Lisää suosikkeihin</string>
@@ -415,6 +418,7 @@
     <string name="new_notification">Uusi Ilmoitus</string>
     <string name="new_version_was_created">Luotiin uusi versio</string>
     <string name="no_browser_available">Linkkien käsittelyyn ei ole sovellusta</string>
+    <string name="no_mutliple_accounts_allowed">Vain yksi tili on sallittu</string>
     <string name="no_pdf_app_available">PDF -sovellusta ei löydy</string>
     <string name="note_confirm">Lähetä</string>
     <string name="note_could_not_sent">Huomiota ei voitu lähettää</string>

+ 3 - 0
src/main/res/values-fr/strings.xml

@@ -268,6 +268,7 @@ Attention la suppression est irréversible.</string>
     <string name="favorite_icon">Favoris</string>
     <string name="feedback_no_mail_app">Aucune application disponible pour envoyer des mails !</string>
     <string name="file_delete">Supprimer</string>
+    <string name="file_detail_activity_error">Erreur lors de la récupération de l’activité du fichier</string>
     <string name="file_details_no_content">Impossible de charger les détails</string>
     <string name="file_icon">Fichier</string>
     <string name="file_keep">Conserver</string>
@@ -421,6 +422,7 @@ Attention la suppression est irréversible.</string>
     <string name="new_notification">Nouvelle notification</string>
     <string name="new_version_was_created">Une nouvelle version a été créée</string>
     <string name="no_browser_available">Aucune application n\'est disponible pour ouvrir les liens</string>
+    <string name="no_mutliple_accounts_allowed">Seulement un compte autorisé</string>
     <string name="no_pdf_app_available">Aucune application n\'est disponible pour ouvrir les fichiers PDF</string>
     <string name="note_confirm">Envoyer</string>
     <string name="note_could_not_sent">Impossible d\'envoyer la note</string>
@@ -538,6 +540,7 @@ Attention la suppression est irréversible.</string>
     <string name="recommend_subject">Essayez %1$s sur votre appareil !</string>
     <string name="recommend_text">J\'aimerais vous inviter à utiliser %1$s sur votre appareil.\nTéléchargez-la ici : %2$s</string>
     <string name="recommend_urls">%1$s ou %2$s</string>
+    <string name="remote_file_fetch_failed">Impossible de trouver le fichier</string>
     <string name="remove_fail_msg">Échec de la suppression</string>
     <string name="remove_notification_failed">Erreur lors de la suppression des notifications.</string>
     <string name="remove_push_notification">Supprimer</string>

+ 3 - 0
src/main/res/values-gl/strings.xml

@@ -266,6 +266,7 @@
     <string name="favorite_icon">Favorito</string>
     <string name="feedback_no_mail_app">Non hai ningunha aplicación para enviar correos!</string>
     <string name="file_delete">Eliminar</string>
+    <string name="file_detail_activity_error">Produciuse un erro ao recuperar actividades para o ficheiro</string>
     <string name="file_details_no_content">Produciuse un fallo ao cargar os detalles</string>
     <string name="file_icon">Ficheiro</string>
     <string name="file_keep">Manter</string>
@@ -419,6 +420,7 @@
     <string name="new_notification">Nova notificación</string>
     <string name="new_version_was_created">Creouse unha versión nova</string>
     <string name="no_browser_available">Non hai ningunha aplicación para manexar ligazóns</string>
+    <string name="no_mutliple_accounts_allowed">Só se permite unha conta</string>
     <string name="no_pdf_app_available">Non hai ningunha aplicación para manexar PDF</string>
     <string name="note_confirm">Enviar</string>
     <string name="note_could_not_sent">Non foi posíbel enviar a nota</string>
@@ -537,6 +539,7 @@
     <string name="recommend_subject">Probe %1$s no seu dispositivo!</string>
     <string name="recommend_text">Quixera convidalo a usar %1$s no seu dispositivo\nDescargueo aquí: %2$s</string>
     <string name="recommend_urls">%1$s ou %2$s</string>
+    <string name="remote_file_fetch_failed">Produciuse un fallo ao atopar o ficheiro!</string>
     <string name="remove_fail_msg">Produciuse un fallo na eliminación</string>
     <string name="remove_notification_failed">Produciuse un fallo ao retirar a notificación.</string>
     <string name="remove_push_notification">Retirar</string>

+ 3 - 0
src/main/res/values-it/strings.xml

@@ -266,6 +266,7 @@
     <string name="favorite_icon">Preferito</string>
     <string name="feedback_no_mail_app">Nessun applicazione disponibile per inviare email!</string>
     <string name="file_delete">Elimina</string>
+    <string name="file_detail_activity_error">Errore durante il recupero delle attività per i file</string>
     <string name="file_details_no_content">Caricamento dettagli non riuscito</string>
     <string name="file_icon">File</string>
     <string name="file_keep">Mantieni</string>
@@ -419,6 +420,7 @@
     <string name="new_notification">Nuova notifica</string>
     <string name="new_version_was_created">Una nuova versione è stata creata</string>
     <string name="no_browser_available">Nessun applicazione disponibile per gestire i collegamenti</string>
+    <string name="no_mutliple_accounts_allowed">Solo un account consentito</string>
     <string name="no_pdf_app_available">Nessun applicazione disponibile per gestire i PDF</string>
     <string name="note_confirm">Invia</string>
     <string name="note_could_not_sent">Impossibile inviare la nota</string>
@@ -536,6 +538,7 @@
     <string name="recommend_subject">Prova %1$s sul tuo dispositivo!</string>
     <string name="recommend_text">Vorrei invitarti a utilizzare %1$s sul tuo dispositivo.\nScaricalo qui:%2$s</string>
     <string name="recommend_urls">%1$s o %2$s</string>
+    <string name="remote_file_fetch_failed">File non trovato!</string>
     <string name="remove_fail_msg">Eliminazione non riuscita</string>
     <string name="remove_notification_failed">Rimozione notifica non riuscita.</string>
     <string name="remove_push_notification">Rimuovi</string>

+ 1 - 0
src/main/res/values-ja-rJP/strings.xml

@@ -202,6 +202,7 @@
     <string name="downloader_download_succeeded_ticker">ダウンロード済</string>
     <string name="downloader_not_downloaded_yet">未ダウンロード</string>
     <string name="drawer_close">サイドバーを閉じる</string>
+    <string name="drawer_community">コミュニティ</string>
     <string name="drawer_current_account">現在のアカウント</string>
     <string name="drawer_end_account">最後のアカウント</string>
     <string name="drawer_header_background">ドロワーヘッダーの背景イメージ</string>

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

@@ -266,6 +266,7 @@
     <string name="favorite_icon">Favoriet</string>
     <string name="feedback_no_mail_app">Geen app beschikbaar voor versturen mail!</string>
     <string name="file_delete">Verwijderen</string>
+    <string name="file_detail_activity_error">Fout bij ophalen activiteiten van bestand</string>
     <string name="file_details_no_content">Kon details niet laden</string>
     <string name="file_icon">Bestand</string>
     <string name="file_keep">Behouden</string>
@@ -419,6 +420,7 @@
     <string name="new_notification">Nieuwe Melding</string>
     <string name="new_version_was_created">Nieuwe versie gecreëerd</string>
     <string name="no_browser_available">Geen app beschikbaar voor het openen van de link</string>
+    <string name="no_mutliple_accounts_allowed">Slechts één account toegestaan</string>
     <string name="no_pdf_app_available">Geen app bechikbaar voor het openen van de pdf</string>
     <string name="note_confirm">Versturen</string>
     <string name="note_could_not_sent">Kon notitie niet versturen</string>

+ 4 - 1
src/main/res/values-pl/strings.xml

@@ -164,7 +164,7 @@
     <string name="copy_file_invalid_into_descendent">Nie można skopiować katalogu do jednego z jego podkatalogów</string>
     <string name="copy_file_invalid_overwrite">Plik już istnieje w katalogu docelowym</string>
     <string name="copy_file_not_found">Nie można skopiować. Sprawdź, czy plik istnieje</string>
-    <string name="copy_internal_link">Skopiuj link wewnętrzny</string>
+    <string name="copy_internal_link">Kopiuj link wewnętrzny</string>
     <string name="copy_internal_link_subline">Działa tylko dla użytkowników mających dostęp do tego katalogu</string>
     <string name="copy_link">Kopiuj link</string>
     <string name="copy_move_to_encrypted_folder_not_supported">Kopiuj/przenieś do zaszyfrowanego katalogu, który nie jest obecnie obsługiwany.</string>
@@ -266,6 +266,7 @@
     <string name="favorite_icon">Ulubiony</string>
     <string name="feedback_no_mail_app">Brak aplikacji do wysyłania wiadomości!</string>
     <string name="file_delete">Usuń</string>
+    <string name="file_detail_activity_error">Błąd podczas pobierania aktywności dla pliku</string>
     <string name="file_details_no_content">Nie udało się załadować szczegółów</string>
     <string name="file_icon">Plik</string>
     <string name="file_keep">Zachowaj</string>
@@ -419,6 +420,7 @@
     <string name="new_notification">Nowe powiadomienie</string>
     <string name="new_version_was_created">Utworzono nową wersję</string>
     <string name="no_browser_available">Brak aplikacji obsługującej odnośniki</string>
+    <string name="no_mutliple_accounts_allowed">Dozwolone jest tylko jedno konto</string>
     <string name="no_pdf_app_available">Brak aplikacji obsługującej PDF</string>
     <string name="note_confirm">Wyślij</string>
     <string name="note_could_not_sent">Nie można wysłać notatki</string>
@@ -536,6 +538,7 @@
     <string name="recommend_subject">Wypróbuj %1$s na swoim urządzeniu!</string>
     <string name="recommend_text">Chciałbym zaprosić Ciebie do użytkowania z %1$s na swoim urządzeniu.\nPobierz stąd: %2$s</string>
     <string name="recommend_urls">%1$s lub %2$s</string>
+    <string name="remote_file_fetch_failed">Nie znaleziono pliku!</string>
     <string name="remove_fail_msg">Usuwanie nie powiodło się</string>
     <string name="remove_notification_failed">Usunięcie powiadomienia nie powiodło się.</string>
     <string name="remove_push_notification">Usuń</string>

+ 3 - 0
src/main/res/values-pt-rBR/strings.xml

@@ -266,6 +266,7 @@
     <string name="favorite_icon">Favorito</string>
     <string name="feedback_no_mail_app">Sem aplicativo para enviar e-mails!</string>
     <string name="file_delete">Excluir</string>
+    <string name="file_detail_activity_error">Erro ao recuperar atividades do arquivo</string>
     <string name="file_details_no_content">Falha ao carregar detalhes</string>
     <string name="file_icon">Arquivo</string>
     <string name="file_keep">Manter</string>
@@ -419,6 +420,7 @@
     <string name="new_notification">Nova notificação</string>
     <string name="new_version_was_created">Nova versão criada</string>
     <string name="no_browser_available">Nenhum aplicativo disponível para lidar com links</string>
+    <string name="no_mutliple_accounts_allowed">Somente uma conta é permitida</string>
     <string name="no_pdf_app_available">Nenhum aplicativo disponível para lidar com PDF</string>
     <string name="note_confirm">Enviar</string>
     <string name="note_could_not_sent">Não foi possível enviar a anotação</string>
@@ -536,6 +538,7 @@
     <string name="recommend_subject">Experimente %1$s em seu dispositivo!</string>
     <string name="recommend_text">Quero convidar você a usar %1$s em seu dispositivo.\nBaixe daqui: %2$s</string>
     <string name="recommend_urls">%1$s ou %2$s</string>
+    <string name="remote_file_fetch_failed">Erro ao encontrar arquivo!</string>
     <string name="remove_fail_msg">Falha na exclusão</string>
     <string name="remove_notification_failed">Erro ao remover a notificação.</string>
     <string name="remove_push_notification">Remover</string>

+ 9 - 0
src/main/res/values-ro/strings.xml

@@ -28,6 +28,7 @@
     <string name="actionbar_sync">Reîmprospătează contul</string>
     <string name="active_user">Utilizator activ</string>
     <string name="activities_no_results_headline">Momentan fără activitate</string>
+    <string name="activities_no_results_message">Nu există evenimente precum adăugări, schimbări sau partajări deocamdată</string>
     <string name="activity_chooser_send_file_title">Trimite</string>
     <string name="activity_chooser_title">Trimite link la…</string>
     <string name="activity_icon">Activitate</string>
@@ -71,6 +72,8 @@
     <string name="auth_username">Nume utilizator</string>
     <string name="auth_wrong_connection_title">Nu s-a putut stabili conexiunea</string>
     <string name="auth_wtf_reenter_URL">Situație neașteptată; vă rugăm să reintroduceți adresa serverului</string>
+    <string name="authentication_exception">Eroare de autentificare</string>
+    <string name="auto_upload_file_behaviour_kept_in_folder">pastrează în directorul original deoarece este disponibil doar pentru citire</string>
     <string name="auto_upload_on_wifi">Se încarcă numai pe Wi-Fi nelimitat</string>
     <string name="auto_upload_path">/AutoÎncărcare</string>
     <string name="autoupload_create_new_custom_folder">Crează un director personalizat</string>
@@ -80,8 +83,11 @@
     <string name="battery_optimization_close">Închidere</string>
     <string name="battery_optimization_disable">Dezactivează</string>
     <string name="battery_optimization_message">Dispozitivul dumneavoastră poate avea economizorul de baterie activat. Incărcarea automată AutoUpload funcționează corespunzător daca excludeti această funcție din aplicație.</string>
+    <string name="battery_optimization_no_setting">Nu am putut deschide setările bateriei. Te rog modifică manual în setări.</string>
     <string name="battery_optimization_title">Optimizare baterie</string>
     <string name="certificate_load_problem">Există o problemă la încărcarea certificatului.</string>
+    <string name="changelog_dev_version">Jurnalul de modificări pentru versiunea în curs de dezvoltare</string>
+    <string name="checkbox">Căsuța de bifat</string>
     <string name="choose_local_folder">Alege dosar local…</string>
     <string name="choose_remote_folder">Alege dosar la distanță…</string>
     <string name="clear_notifications_failed">Eroare la ștergerea notificărilor.</string>
@@ -109,8 +115,11 @@
     <string name="common_switch_account">Schimbă contul</string>
     <string name="common_unknown">necunoscut</string>
     <string name="common_yes">Da</string>
+    <string name="community_beta_headline">Testează versiunea în curs de dezvoltare</string>
     <string name="community_contribute_forum_forum">forum</string>
     <string name="community_contribute_forum_text">Ajută pe altii la </string>
+    <string name="community_contribute_headline">Contribuie în mod activ</string>
+    <string name="community_contribute_irc_text">Alătură-te conversației pe IRC:</string>
     <string name="community_contribute_translate_text">aplicația </string>
     <string name="community_contribute_translate_translate">tradu</string>
     <string name="configure_new_media_folder_detection_notifications">Configurează</string>

+ 5 - 2
src/main/res/values-ru/strings.xml

@@ -106,7 +106,7 @@
     <string name="common_loading">Загрузка…</string>
     <string name="common_no">Нет</string>
     <string name="common_ok">ОК</string>
-    <string name="common_pending">В очереди</string>
+    <string name="common_pending">Ожидается</string>
     <string name="common_remove">Удалить</string>
     <string name="common_rename">Переименовать</string>
     <string name="common_save">Сохранить</string>
@@ -169,6 +169,7 @@
     <string name="copy_link">Копировать ссылку</string>
     <string name="copy_move_to_encrypted_folder_not_supported">Копирование и перемещение в зашифрованные папки пока не поддерживается.</string>
     <string name="copy_to">Копировать…</string>
+    <string name="could_not_download_image">Не удалось скачать полноразмерное изображение</string>
     <string name="could_not_retrieve_url">Не удалось получить URL-адрес</string>
     <string name="create_dir_fail_msg">Не удалось создать каталог</string>
     <string name="create_file_from_template">Создание файла на основе шаблона…</string>
@@ -258,12 +259,14 @@
     <string name="etm_title">Режим разработчика</string>
     <string name="fab_label">Добавить или выгрузить</string>
     <string name="failed_to_download">Не удалось передать файл в диспетчер загрузок</string>
+    <string name="failed_to_print">Ошибка печати файла</string>
     <string name="fallback_weblogin_back">Назад</string>
     <string name="fallback_weblogin_text">Использовать прежний метод авторизации</string>
     <string name="favorite">Добавить в избранное</string>
     <string name="favorite_icon">Избранное</string>
     <string name="feedback_no_mail_app">Отсутствует приложение для работы с эл. почтой.</string>
     <string name="file_delete">Удалить</string>
+    <string name="file_detail_activity_error">Ошибка получения истории событий, связанных с файлом</string>
     <string name="file_details_no_content">Не удалось получить подробные сведения</string>
     <string name="file_icon">Файл</string>
     <string name="file_keep">Сохранить</string>
@@ -546,7 +549,7 @@
     <string name="resized_image_not_possible">Отсутствует оптимизированное изображение.</string>
     <string name="resized_image_not_possible_download">Оптимизированное изображение отсутствует. Скачать изображение исходного размера?</string>
     <string name="restore">Восстановить файл</string>
-    <string name="restore_button_description">Восстановить удаленный файл</string>
+    <string name="restore_button_description">Восстановить удалённый файл</string>
     <string name="retrieving_file">Получение файла…</string>
     <string name="richdocuments_failed_to_load_document">Не удалось загрузить документ.</string>
     <string name="saml_authentication_required_text">Требуется аутентификация</string>

+ 3 - 0
src/main/res/values-sk-rSK/strings.xml

@@ -266,6 +266,7 @@
     <string name="favorite_icon">Obľúbené</string>
     <string name="feedback_no_mail_app">Aplikácia na odoslanie e-mailu nenájdená!</string>
     <string name="file_delete">Zmazať</string>
+    <string name="file_detail_activity_error">Chyba pri načítavaní aktivít pre súbor</string>
     <string name="file_details_no_content">Nepodarilo sa načítať podrobnosti</string>
     <string name="file_icon">Súbor</string>
     <string name="file_keep">Ponechať</string>
@@ -419,6 +420,7 @@
     <string name="new_notification">Nové upozornenie</string>
     <string name="new_version_was_created">Nová verzia bola vytvorená</string>
     <string name="no_browser_available">Aplikácia na prácu s linkami nenájdená</string>
+    <string name="no_mutliple_accounts_allowed">Povolený je iba jeden účet</string>
     <string name="no_pdf_app_available">Aplikácia na prácu s PDF nenájdená</string>
     <string name="note_confirm">Odoslať</string>
     <string name="note_could_not_sent">Nebolo možné odoslať poznámku</string>
@@ -536,6 +538,7 @@
     <string name="recommend_subject">Vyskúšajte %1$s na vašom zariadení!</string>
     <string name="recommend_text">Chcel by som odporučiť %1$s pre vaše zariadenie.\nZískať ho môžete aj tu: %2$s</string>
     <string name="recommend_urls">%1$s alebo %2$s</string>
+    <string name="remote_file_fetch_failed">Nepodarilo sa nájsť súbor</string>
     <string name="remove_fail_msg">Zmazanie zlyhalo</string>
     <string name="remove_notification_failed">Odstránenie upozornenia zlyhalo.</string>
     <string name="remove_push_notification">Odstrániť</string>

+ 32 - 0
src/main/res/values-sl/strings.xml

@@ -153,6 +153,8 @@
     <string name="contacts_last_backup">Zadnje shranjevanje</string>
     <string name="contacts_preference_backup_never">nikoli</string>
     <string name="contacts_preference_choose_date">Izberi datum</string>
+    <string name="contacts_preferences_backup_scheduled">Ustvarjanje varnostne kopije je načrtovano in se bo kmalu začelo.</string>
+    <string name="contacts_preferences_import_scheduled">Uvoz je načrtovan in se bo kmalu začel.</string>
     <string name="contacts_preferences_no_file_found">Ne najdem datoteke</string>
     <string name="contacts_preferences_something_strange_happened">Zadnje varnostne kopije ni mogoče najti!</string>
     <string name="contacts_read_permission">Zahtevano je dovoljenje za branje seznama stikov</string>
@@ -166,6 +168,7 @@
     <string name="copy_link">Kopiraj povezavo</string>
     <string name="copy_move_to_encrypted_folder_not_supported">Kopiranje/Premikanje predmetov v šifrirano mapo trenutno ni podprto.</string>
     <string name="copy_to">Kopiraj v ...</string>
+    <string name="could_not_download_image">Slike ni mogoče prejeti v celoti.</string>
     <string name="could_not_retrieve_url">Naslova URL ni mogoče pridobiti</string>
     <string name="create_dir_fail_msg">Ni mogoče ustvariti mape</string>
     <string name="create_file_from_template">Poteka ustvarjanje dokumenta iz predloge ...</string>
@@ -177,6 +180,7 @@
     <string name="date_unknown">Neznano</string>
     <string name="default_credentials_wrong">Nepravilna poverila</string>
     <string name="delete_account">Odstrani račun</string>
+    <string name="delete_account_warning">Ali želite odstraniti račun %s z vsemi krajevnimi datotekami?\n\nBrisanja kasneje ni mogoče razveljaviti.</string>
     <string name="delete_entries">Izbriši vnose</string>
     <string name="deselect_all">Odstrani celoten izbor</string>
     <string name="dev_version_new_version_available">Na voljo je nova različic</string>
@@ -226,6 +230,7 @@
     <string name="end_to_end_encryption_confirm_button">Nastavitev šifriranja</string>
     <string name="end_to_end_encryption_decrypting">Poteka odšifriranje ...</string>
     <string name="end_to_end_encryption_dialog_close">Zapri</string>
+    <string name="end_to_end_encryption_enter_password">Vpišite geslo za odšifriranje zasebnega ključa.</string>
     <string name="end_to_end_encryption_folder_not_empty">Mapa ni prazna.</string>
     <string name="end_to_end_encryption_generating_keys">Poteka ustvarjanje novih ključev ...</string>
     <string name="end_to_end_encryption_password">Geslo …</string>
@@ -388,12 +393,27 @@
     <string name="new_media_folder_videos">video</string>
     <string name="new_notification">Novo obvestilo</string>
     <string name="new_version_was_created">Ustvarjena je nova različica</string>
+    <string name="no_browser_available">Ni nameščenega programa, ki upravlja s povezavami</string>
+    <string name="no_mutliple_accounts_allowed">Dovoljen je le en račun</string>
+    <string name="no_pdf_app_available">Ni nameščenega programa za odpiranje datotek PDF</string>
     <string name="note_confirm">Pošlji</string>
+    <string name="notification_action_failed">Izvedba dejanja je spodletela</string>
+    <string name="notification_channel_download_description">Pokaži napredek prejemanja</string>
     <string name="notification_channel_file_observer_description">Preverjaj datoteke za spremembe</string>
+    <string name="notification_channel_file_observer_name">Opazovalnik datotek</string>
+    <string name="notification_channel_file_sync_description">Pokaži napredek usklajevanja datotek in končana opravila</string>
     <string name="notification_channel_file_sync_name">Usklajevanje datotek</string>
+    <string name="notification_channel_general_name">Splošna obvestila</string>
     <string name="notification_channel_media_name">Predvajalnik predstavnih datotek</string>
+    <string name="notification_channel_push_name">Potisna obvestila</string>
+    <string name="notification_channel_upload_description">Pokaži napredek pošiljanja</string>
+    <string name="notification_channel_upload_name">Kanal obvestil pošiljanja</string>
+    <string name="notification_icon">Obvestilna ikona</string>
+    <string name="notifications_loading_activity">Nalaganje obvestil ...</string>
     <string name="notifications_no_results_headline">Ni obvestil</string>
     <string name="notifications_no_results_message">Prosim, preveri kasneje</string>
+    <string name="offline_mode">Ni vzpostavljene internetne povezave</string>
+    <string name="operation_canceled">Opravilo je preklicano</string>
     <string name="participate_beta_headline">Preizkušanje razvojne različice</string>
     <string name="participate_beta_text">To vključuje vse prihajajoče zmožnosti programa. Hrošči in napake se pojavljajo in ko se, je priporočljivo poslati poročilo za razhroščevanje.</string>
     <string name="participate_contribute_forum_forum">forumu</string>
@@ -470,6 +490,7 @@
     <string name="recommend_text">Želim priporočiti program %1$s!\nPrejeti ga je mogoče prek: %2$s</string>
     <string name="recommend_urls">%1$s ali %2$s</string>
     <string name="remove_fail_msg">Brisanje je spodletelo</string>
+    <string name="remove_notification_failed">Odstranjevanje obvestil je spodletelo.</string>
     <string name="remove_push_notification">Odstrani</string>
     <string name="remove_success_msg">Izbrisano</string>
     <string name="rename_dialog_title">Vnesite novo ime</string>
@@ -482,10 +503,17 @@
     <string name="saml_authentication_required_text">Zahtevano je geslo</string>
     <string name="saml_authentication_wrong_pass">Napačno geslo</string>
     <string name="scanQR_description">Prijava s kodo QR</string>
+    <string name="screenshot_04_accounts_heading">Vsi vaši računi</string>
+    <string name="screenshot_04_accounts_subline">na enem mestu</string>
     <string name="screenshot_05_autoUpload_heading">Samodejno pošiljanje</string>
+    <string name="screenshot_05_autoUpload_subline">za vaše slike in posnetke</string>
+    <string name="screenshot_06_davdroid_heading">Usklajevalnik koledarja in stikov</string>
+    <string name="screenshot_06_davdroid_subline">z DAVx5 (prej poimenovan DAVdroid)</string>
     <string name="search_users_and_groups_hint">Iskanje uporabnikov in skupin</string>
     <string name="select_all">Izberi vse</string>
+    <string name="select_template">IZbor predloge</string>
     <string name="send">Pošlji</string>
+    <string name="send_note">Pošlji sporočilo prejemniku</string>
     <string name="set_as">Nastavi kot</string>
     <string name="set_picture_as">Uporabi sliko kot</string>
     <string name="setup_btn_connect">Poveži</string>
@@ -513,6 +541,7 @@
     <string name="share_remote_clarification">%1$s (oddaljeno)</string>
     <string name="share_via_link_edit_permission_label">Dovoli urejanje</string>
     <string name="share_via_link_expiration_date_label">Nastavi datum preteka</string>
+    <string name="share_via_link_hide_download">Skrij prejem</string>
     <string name="share_via_link_hide_file_listing_permission_label">Skrij spisek datotek</string>
     <string name="share_via_link_password_label">Zaščiti z geslom</string>
     <string name="share_via_link_section_title">Omogoči souporabo prek povezave</string>
@@ -561,6 +590,7 @@
     <string name="storage_movies">Video</string>
     <string name="storage_music">Glasba</string>
     <string name="storage_pictures">Slike</string>
+    <string name="stream_not_possible_headline">Notranji pretok ni mogoč</string>
     <string name="subject_shared_with_you">\"%1$s\" vam je oddan v souporabo</string>
     <string name="subject_user_shared_with_you">Uporabnik %1$s je omogočil souporabo \"%2$s\"</string>
     <string name="sync_conflicts_in_favourites_ticker">Zaznani spori</string>
@@ -588,6 +618,7 @@
     <string name="timeout_richDocuments">Nalaganje poteka nepričakovano dolgo ...</string>
     <string name="trashbin_activity_title">Izbrisane datoteke</string>
     <string name="trashbin_empty_headline">Ni izbrisanih datotek</string>
+    <string name="trashbin_file_not_restored">Datoteke %1$s ni mogoče obnoviti!</string>
     <string name="unset_encrypted">Odstrani šifriranje</string>
     <string name="unshare_link_file_error">Prišlo je do napake med poskusom odstranjevanja souporabe te datoteke ali mape</string>
     <string name="unshare_link_file_no_exist">Ni mogoče prekiniti souporabe. Preverite, ali datoteka obstaja.</string>
@@ -595,6 +626,7 @@
     <string name="update_link_file_error">Prišlo je do napake med posodabljanjem mesta souporabe</string>
     <string name="update_link_file_no_exist">Posodobitev ni mogoča. Preveri, ali datoteka obstaja.</string>
     <string name="update_link_forbidden_permissions">za posodobitev mesta souporabe</string>
+    <string name="upload_cannot_create_file">Krajevne datoteke ni mogoče ustvariti</string>
     <string name="upload_chooser_title">Pošlji iz …</string>
     <string name="upload_content_from_other_apps">Pošlji vsebino iz drugih programov</string>
     <string name="upload_file_dialog_filename">Ime datoteke</string>

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

@@ -267,6 +267,7 @@
     <string name="favorite_icon">Омиљени</string>
     <string name="feedback_no_mail_app">Нема апликација за слање е-поште!</string>
     <string name="file_delete">Обриши</string>
+    <string name="file_detail_activity_error">Грешка при дохватању активности фајла</string>
     <string name="file_details_no_content">Грешка при учитавању детаља</string>
     <string name="file_icon">Фајл</string>
     <string name="file_keep">Задржи</string>
@@ -420,6 +421,7 @@
     <string name="new_notification">Ново обавештење</string>
     <string name="new_version_was_created">Направљена је нова верзија</string>
     <string name="no_browser_available">Нема апликације која зна да отвори везу</string>
+    <string name="no_mutliple_accounts_allowed">Дозвољен је само један налог</string>
     <string name="no_pdf_app_available">Нема апликације која зна да ради са PDF-овима</string>
     <string name="note_confirm">Пошаљи</string>
     <string name="note_could_not_sent">Не могу да пошаљем белешку</string>

+ 3 - 0
src/main/res/values-sv/strings.xml

@@ -265,6 +265,7 @@
     <string name="favorite_icon">Favoritisera</string>
     <string name="feedback_no_mail_app">Ingen app finns att skicka e-post!</string>
     <string name="file_delete">Radera</string>
+    <string name="file_detail_activity_error">Fel vid hämtning av aktiviteter för fil</string>
     <string name="file_details_no_content">Kunde inte ladda detaljer</string>
     <string name="file_icon">Fil</string>
     <string name="file_keep">Behåll</string>
@@ -418,6 +419,7 @@
     <string name="new_notification">Nytt meddelande</string>
     <string name="new_version_was_created">Ny version skapades</string>
     <string name="no_browser_available">Ingen app tillgänglig för att hantera länkar</string>
+    <string name="no_mutliple_accounts_allowed">Endast ett konto tillåtet</string>
     <string name="no_pdf_app_available">Ingen app tillgänglig för att hantera PDF</string>
     <string name="note_confirm">Skicka</string>
     <string name="note_could_not_sent">Kunde inte skicka meddelande</string>
@@ -535,6 +537,7 @@
     <string name="recommend_subject">Försök%1$s på din enhet!</string>
     <string name="recommend_text">Jag vill bjuda in dig att använda %1$s på din enhet.\nLadda ner här: %2$s</string>
     <string name="recommend_urls">%1$s eller %2$s</string>
+    <string name="remote_file_fetch_failed">Kunde inte hitta filen!</string>
     <string name="remove_fail_msg">Radering misslyckades</string>
     <string name="remove_notification_failed">Kunde inte radera notifiering.</string>
     <string name="remove_push_notification">Ta bort</string>

+ 3 - 0
src/main/res/values-tr/strings.xml

@@ -266,6 +266,7 @@
     <string name="favorite_icon">Sık Kullanılanlara Ekle</string>
     <string name="feedback_no_mail_app">E-posta göndermek için kullanılabilecek bir uygulama yok!</string>
     <string name="file_delete">Sil</string>
+    <string name="file_detail_activity_error">Dosya işlemleri alınırken sorun çıktı</string>
     <string name="file_details_no_content">Ayrıntılar yüklenemedi</string>
     <string name="file_icon">Dosya</string>
     <string name="file_keep">Tut</string>
@@ -419,6 +420,7 @@
     <string name="new_notification">Yeni Bildirim</string>
     <string name="new_version_was_created">Yeni sürüm oluşturuldu</string>
     <string name="no_browser_available">Bağlantıları işleyecek bir uygulama bulunamadı</string>
+    <string name="no_mutliple_accounts_allowed">Yalnız bir hesap kullanılabilir</string>
     <string name="no_pdf_app_available">PDF dosyalarını işleyecek bir uygulama bulunamadı</string>
     <string name="note_confirm">Gönder</string>
     <string name="note_could_not_sent">Not gönderilemedi</string>
@@ -536,6 +538,7 @@
     <string name="recommend_subject">%1$s uygulamasını aygıtınız üzerinde deneyin!</string>
     <string name="recommend_text">Aygıtında %1$s kullanmanı öneriyorum!\nŞuradan indirebilirsin: %2$s</string>
     <string name="recommend_urls">%1$s ya da %2$s</string>
+    <string name="remote_file_fetch_failed">Dosya bulunamadı!</string>
     <string name="remove_fail_msg">Silinemedi</string>
     <string name="remove_notification_failed">Bildirim silinemedi.</string>
     <string name="remove_push_notification">Sil</string>

+ 0 - 5
src/main/res/values-v21/styles.xml

@@ -79,9 +79,4 @@
         <item name="colorControlNormal">@color/login_text_color</item>
         <item name="colorControlActivated">@color/login_text_color</item>
     </style>
-
-    <style name="Nextcloud.EditText.Generic" parent="ThemeOverlay.MaterialComponents.TextInputEditText">
-        <item name="colorControlNormal">@color/login_text_color</item>
-        <item name="colorControlActivated">@color/login_text_color</item>
-    </style>
 </resources>

+ 13 - 0
src/main/res/values-zh-rTW/strings.xml

@@ -169,6 +169,7 @@
     <string name="copy_link">複製連結</string>
     <string name="copy_move_to_encrypted_folder_not_supported">不支援複製移動加密的資料夾</string>
     <string name="copy_to">複製到…</string>
+    <string name="could_not_download_image">無法下載完整圖片</string>
     <string name="could_not_retrieve_url">無法存取網址</string>
     <string name="create_dir_fail_msg">無法新增資料夾</string>
     <string name="create_file_from_template">建立範本…</string>
@@ -201,6 +202,7 @@
     <string name="downloader_download_succeeded_ticker">已下載</string>
     <string name="downloader_not_downloaded_yet">尚未下載</string>
     <string name="drawer_close">關閉側邊攔</string>
+    <string name="drawer_community">社群</string>
     <string name="drawer_current_account">目前帳戶</string>
     <string name="drawer_end_account">最後帳號</string>
     <string name="drawer_item_activities">活動</string>
@@ -256,12 +258,14 @@
     <string name="etm_title">工程測試模式</string>
     <string name="fab_label">新增或上傳</string>
     <string name="failed_to_download">無法下載</string>
+    <string name="failed_to_print">列印失敗</string>
     <string name="fallback_weblogin_back">返回</string>
     <string name="fallback_weblogin_text">還原舊的登入方式</string>
     <string name="favorite">加到我的最愛</string>
     <string name="favorite_icon">我的最愛</string>
     <string name="feedback_no_mail_app">沒有應用程式可以傳送信件!</string>
     <string name="file_delete">刪除</string>
+    <string name="file_detail_activity_error">檢所檔案活動時發生錯誤</string>
     <string name="file_details_no_content">載入詳細資訊失敗</string>
     <string name="file_icon">檔案</string>
     <string name="file_keep">保留</string>
@@ -326,6 +330,7 @@
     <string name="filename_hint">檔名</string>
     <string name="files_drop_not_supported">這是 Nextcloud 的功能,請更新</string>
     <string name="first_run_1_text">在您的控制下維持資料的安全</string>
+    <string name="first_run_2_text">安全地協作 &amp; 傳輸檔案</string>
     <string name="first_run_3_text">容易使用的Webmail,行事曆&amp;聯絡人</string>
     <string name="first_run_4_text">分享螢幕畫面,線上會議&amp;研討會</string>
     <string name="folder_already_exists">文件夾已存在</string>
@@ -370,6 +375,7 @@
     <string name="logs_menu_send">藉由e-mail傳送紀錄</string>
     <string name="logs_status_filtered">記錄:%1$dkB,符合查詢%2$d/%3$d,耗時%4$dms</string>
     <string name="logs_status_loading">載入中…</string>
+    <string name="logs_status_not_filtered">記錄檔: %1$d KB(不篩選)</string>
     <string name="logs_title">紀錄檔</string>
     <string name="maintenance_mode">伺服器正處於維護模式</string>
     <string name="manage_space_clear_data">清除資料</string>
@@ -413,6 +419,7 @@
     <string name="new_notification">新的通知</string>
     <string name="new_version_was_created">新版本已建立</string>
     <string name="no_browser_available">沒有應用程式可以開啟連結</string>
+    <string name="no_mutliple_accounts_allowed">只可使用一個帳戶</string>
     <string name="no_pdf_app_available">沒有應用程式可以開啟 PDF</string>
     <string name="note_confirm">傳送</string>
     <string name="note_could_not_sent">傳送留言失敗</string>
@@ -492,6 +499,8 @@
     <string name="prefs_category_general">一般</string>
     <string name="prefs_category_more">更多</string>
     <string name="prefs_daily_contacts_sync_summary">每日備份通訊錄</string>
+    <string name="prefs_e2e_mnemonic">端到端助記碼</string>
+    <string name="prefs_e2e_no_device_credentials">裝置若要顯示註記請啟用裝置憑證</string>
     <string name="prefs_enable_media_scan_notifications">顯示媒體掃描通知</string>
     <string name="prefs_enable_media_scan_notifications_summary">發現新的多媒體資料夾時通知</string>
     <string name="prefs_feedback">回饋</string>
@@ -548,8 +557,11 @@
     <string name="saml_authentication_wrong_pass">密碼錯誤</string>
     <string name="scanQR_description">使用 QR Code 登入</string>
     <string name="screenshot_01_gridView_heading">保護您的資料</string>
+    <string name="screenshot_01_gridView_subline">自建具有生產力的平台</string>
     <string name="screenshot_02_listView_heading">瀏覽與分享</string>
     <string name="screenshot_02_listView_subline">全在您一指之間</string>
+    <string name="screenshot_03_drawer_heading">活動,分享,離線檔案</string>
+    <string name="screenshot_03_drawer_subline">可快速存取所有資源</string>
     <string name="screenshot_04_accounts_heading">您所有的帳戶</string>
     <string name="screenshot_04_accounts_subline">在一個地方</string>
     <string name="screenshot_05_autoUpload_heading">自動上傳</string>
@@ -613,6 +625,7 @@
     <string name="shared_icon_shared_via_link">透過連結分享</string>
     <string name="shared_with_you_by">%1$s已經和你分享</string>
     <string name="sharee_add_failed">分享失敗</string>
+    <string name="signup_with_provider">使用第三方登入</string>
     <string name="single_sign_on_request_token" formatted="true">允許%1$s存取您的Nextcloud帳號%2$s?</string>
     <string name="sort_by">排列方式</string>
     <string name="sort_by_modification_date_ascending">最新先</string>

+ 0 - 3
src/main/res/values/setup.xml

@@ -22,9 +22,6 @@
     <!-- URLs and flags related -->
     <string name="server_url"></string>
     <bool name="show_server_url_input">true</bool>
-    <!-- Can be regular (full input), prefix (subdomain input) and suffix (directory input) -->
-    <!-- Requires server url to be set -->
-    <string name="server_input_type">regular</string>
     <bool name="show_provider_or_own_installation">true</bool>
     <string name="provider_registration_server">https://www.nextcloud.com/register</string>
 

+ 3 - 0
src/main/res/values/strings.xml

@@ -900,4 +900,7 @@
     <string name="download_latest_dev_version">Download latest dev version</string>
     <string name="changelog_dev_version">Changelog dev version</string>
     <string name="could_not_download_image">Could not download full image</string>
+    <string name="file_detail_activity_error">Error retrieving activities for file</string>
+    <string name="no_mutliple_accounts_allowed">Only one account allowed</string>
+    <string name="remote_file_fetch_failed">Failed to find file!</string>
 </resources>

+ 107 - 0
src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt

@@ -0,0 +1,107 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * 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.nextcloud.client.jobs
+
+import android.content.ContentResolver
+import android.content.Context
+import android.os.Build
+import androidx.work.WorkerParameters
+import com.nextcloud.client.device.DeviceInfo
+import com.nextcloud.client.device.PowerManagementService
+import com.nextcloud.client.preferences.AppPreferences
+import com.nhaarman.mockitokotlin2.whenever
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import javax.inject.Provider
+
+class BackgroundJobFactoryTest {
+
+    @Mock
+    private lateinit var context: Context
+
+    @Mock
+    private lateinit var params: WorkerParameters
+
+    @Mock
+    private lateinit var contentResolver: ContentResolver
+
+    @Mock
+    private lateinit var preferences: AppPreferences
+
+    @Mock
+    private lateinit var powerManagementService: PowerManagementService
+
+    @Mock
+    private lateinit var backgroundJobManager: BackgroundJobManager
+
+    @Mock
+    private lateinit var deviceInfo: DeviceInfo
+
+    private lateinit var factory: BackgroundJobFactory
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        factory = BackgroundJobFactory(
+            preferences,
+            contentResolver,
+            powerManagementService,
+            Provider { backgroundJobManager },
+            deviceInfo
+        )
+    }
+
+    @Test
+    fun `worker is created on api level 24+`() {
+        // GIVEN
+        //      api level is > 24
+        //      content URI trigger is supported
+        whenever(deviceInfo.apiLevel).thenReturn(Build.VERSION_CODES.N)
+
+        // WHEN
+        //      factory is called to create content observer worker
+        val worker = factory.createWorker(context, ContentObserverWork::class.java.name, params)
+
+        // THEN
+        //      factory creates a worker compatible with API level
+        assertNotNull(worker)
+    }
+
+    @Test
+    fun `worker is not created below api level 24`() {
+        // GIVEN
+        //      api level is < 24
+        //      content URI trigger is not supported
+        whenever(deviceInfo.apiLevel).thenReturn(Build.VERSION_CODES.M)
+
+        // WHEN
+        //      factory is called to create content observer worker
+        val worker = factory.createWorker(context, ContentObserverWork::class.java.name, params)
+
+        // THEN
+        //      factory does not create a worker incompatible with API level
+        assertNull(worker)
+    }
+}

+ 138 - 0
src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt

@@ -0,0 +1,138 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * 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.nextcloud.client.jobs
+
+import android.content.Context
+import android.net.Uri
+import androidx.work.WorkerParameters
+import com.nextcloud.client.device.PowerManagementService
+import com.nhaarman.mockitokotlin2.verify
+import com.nhaarman.mockitokotlin2.whenever
+import com.owncloud.android.datamodel.SyncedFolderProvider
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+class ContentObserverWorkTest {
+
+    private lateinit var worker: ContentObserverWork
+
+    @Mock
+    lateinit var params: WorkerParameters
+
+    @Mock
+    lateinit var context: Context
+
+    @Mock
+    lateinit var folderProvider: SyncedFolderProvider
+
+    @Mock
+    lateinit var powerManagementService: PowerManagementService
+
+    @Mock
+    lateinit var backgroundJobManager: BackgroundJobManager
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        worker = ContentObserverWork(
+            appContext = context,
+            params = params,
+            syncerFolderProvider = folderProvider,
+            powerManagementService = powerManagementService,
+            backgroundJobManager = backgroundJobManager
+        )
+        val uri: Uri = Mockito.mock(Uri::class.java)
+        whenever(params.triggeredContentUris).thenReturn(listOf(uri))
+    }
+
+    @Test
+    fun `job reschedules self after each run unconditionally`() {
+        // GIVEN
+        //      nothing to sync
+        whenever(params.triggeredContentUris).thenReturn(emptyList())
+
+        // WHEN
+        //      worker is called
+        worker.doWork()
+
+        // THEN
+        //      worker reschedules itself unconditionally
+        verify(backgroundJobManager).scheduleContentObserverJob()
+    }
+
+    @Test
+    @Ignore("TODO: needs further refactoring")
+    fun `sync is triggered`() {
+        // GIVEN
+        //      power saving is disabled
+        //      some folders are configured for syncing
+        whenever(powerManagementService.isPowerSavingEnabled).thenReturn(false)
+        whenever(folderProvider.countEnabledSyncedFolders()).thenReturn(1)
+
+        // WHEN
+        //      worker is called
+        worker.doWork()
+
+        // THEN
+        //      sync job is scheduled
+        // TO DO: verify(backgroundJobManager).sheduleFilesSync() or something like this
+    }
+
+    @Test
+    @Ignore("TODO: needs further refactoring")
+    fun `sync is not triggered under power saving mode`() {
+        // GIVEN
+        //      power saving is enabled
+        //      some folders are configured for syncing
+        whenever(powerManagementService.isPowerSavingEnabled).thenReturn(true)
+        whenever(folderProvider.countEnabledSyncedFolders()).thenReturn(1)
+
+        // WHEN
+        //      worker is called
+        worker.doWork()
+
+        // THEN
+        //      sync job is scheduled
+        // TO DO: verify(backgroundJobManager, never()).sheduleFilesSync() or something like this)
+    }
+
+    @Test
+    @Ignore("TODO: needs further refactoring")
+    fun `sync is not triggered if no folder are synced`() {
+        // GIVEN
+        //      power saving is disabled
+        //      no folders configured for syncing
+        whenever(powerManagementService.isPowerSavingEnabled).thenReturn(false)
+        whenever(folderProvider.countEnabledSyncedFolders()).thenReturn(0)
+
+        // WHEN
+        //      worker is called
+        worker.doWork()
+
+        // THEN
+        //      sync job is scheduled
+        // TO DO: verify(backgroundJobManager, never()).sheduleFilesSync() or something like this)
+    }
+}

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