瀏覽代碼

Merge pull request #4890 from nextcloud/directEditing

Direct editing support
Tobias Kaminsky 5 年之前
父節點
當前提交
6af95233fb
共有 39 個文件被更改,包括 1482 次插入756 次删除
  1. 1 1
      scripts/analysis/findbugs-results.txt
  2. 1 1
      scripts/analysis/lint-results.txt
  3. 4 1
      src/main/AndroidManifest.xml
  4. 8 6
      src/main/java/com/nextcloud/client/di/ComponentsModule.java
  5. 3 0
      src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.java
  6. 75 80
      src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  7. 4 0
      src/main/java/com/owncloud/android/datamodel/OCFile.java
  8. 4 1
      src/main/java/com/owncloud/android/datamodel/Template.java
  9. 7 3
      src/main/java/com/owncloud/android/db/ProviderMeta.java
  10. 4 4
      src/main/java/com/owncloud/android/files/FetchTemplateOperation.java
  11. 56 19
      src/main/java/com/owncloud/android/files/FileMenuFilter.java
  12. 34 1
      src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java
  13. 8 1
      src/main/java/com/owncloud/android/operations/RichDocumentsUrlOperation.java
  14. 12 3
      src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java
  15. 40 2
      src/main/java/com/owncloud/android/providers/FileContentProvider.java
  16. 228 0
      src/main/java/com/owncloud/android/ui/activity/EditorWebView.java
  17. 7 0
      src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java
  18. 26 7
      src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  19. 40 165
      src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.java
  20. 67 0
      src/main/java/com/owncloud/android/ui/activity/TextEditorWebView.kt
  21. 66 11
      src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java
  22. 38 42
      src/main/java/com/owncloud/android/ui/asynctasks/LoadUrlTask.java
  23. 4 4
      src/main/java/com/owncloud/android/ui/asynctasks/PrintAsyncTask.java
  24. 2 2
      src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.java
  25. 2 2
      src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java
  26. 42 19
      src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
  27. 24 3
      src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java
  28. 2 0
      src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java
  29. 390 0
      src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java
  30. 16 365
      src/main/java/com/owncloud/android/ui/preview/PreviewTextFragment.java
  31. 174 0
      src/main/java/com/owncloud/android/ui/preview/PreviewTextStringFragment.java
  32. 1 0
      src/main/java/com/owncloud/android/utils/FileStorageUtils.java
  33. 32 0
      src/main/res/drawable/ic_edit.xml
  34. 36 0
      src/main/res/layout/list_header.xml
  35. 0 2
      src/main/res/layout/richdocuments_webview.xml
  36. 16 4
      src/main/res/layout/text_file_preview.xml
  37. 6 6
      src/main/res/menu/item_file.xml
  38. 1 0
      src/main/res/values/setup.xml
  39. 1 1
      src/main/res/values/strings.xml

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

@@ -1 +1 @@
-409
+413

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

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

+ 4 - 1
src/main/AndroidManifest.xml

@@ -133,7 +133,10 @@
         <activity android:name=".ui.activity.ExternalSiteWebView"
                   android:configChanges="orientation|screenSize|keyboardHidden" />
         <activity
-            android:name=".ui.activity.RichDocumentsWebView"
+            android:name=".ui.activity.RichDocumentsEditorWebView"
+            android:configChanges="orientation|screenSize|keyboardHidden" />
+        <activity
+            android:name=".ui.activity.TextEditorWebView"
             android:configChanges="orientation|screenSize|keyboardHidden"/>
         <activity android:name=".ui.activity.ContactsPreferenceActivity"
             android:launchMode="singleInstance"/>

+ 8 - 6
src/main/java/com/nextcloud/client/di/ComponentsModule.java

@@ -54,11 +54,12 @@ import com.owncloud.android.ui.activity.NotificationsActivity;
 import com.owncloud.android.ui.activity.PassCodeActivity;
 import com.owncloud.android.ui.activity.ReceiveExternalFilesActivity;
 import com.owncloud.android.ui.activity.RequestCredentialsActivity;
-import com.owncloud.android.ui.activity.RichDocumentsWebView;
+import com.owncloud.android.ui.activity.RichDocumentsEditorWebView;
 import com.owncloud.android.ui.activity.SettingsActivity;
 import com.owncloud.android.ui.activity.ShareActivity;
 import com.owncloud.android.ui.activity.SsoGrantPermissionActivity;
 import com.owncloud.android.ui.activity.SyncedFoldersActivity;
+import com.owncloud.android.ui.activity.TextEditorWebView;
 import com.owncloud.android.ui.activity.UploadFilesActivity;
 import com.owncloud.android.ui.activity.UploadListActivity;
 import com.owncloud.android.ui.activity.UserInfoActivity;
@@ -76,6 +77,7 @@ import com.owncloud.android.ui.preview.PreviewImageActivity;
 import com.owncloud.android.ui.preview.PreviewImageFragment;
 import com.owncloud.android.ui.preview.PreviewMediaFragment;
 import com.owncloud.android.ui.preview.PreviewTextFragment;
+import com.owncloud.android.ui.preview.PreviewTextStringFragment;
 import com.owncloud.android.ui.preview.PreviewVideoActivity;
 import com.owncloud.android.ui.trashbin.TrashbinActivity;
 
@@ -106,15 +108,12 @@ abstract class ComponentsModule {
     @ContributesAndroidInjector abstract ManageAccountsActivity manageAccountsActivity();
     @ContributesAndroidInjector abstract ManageSpaceActivity manageSpaceActivity();
     @ContributesAndroidInjector abstract NotificationsActivity notificationsActivity();
-
-    @ContributesAndroidInjector
-    abstract CommunityActivity participateActivity();
+    @ContributesAndroidInjector abstract CommunityActivity participateActivity();
     @ContributesAndroidInjector abstract PassCodeActivity passCodeActivity();
     @ContributesAndroidInjector abstract PreviewImageActivity previewImageActivity();
     @ContributesAndroidInjector abstract PreviewVideoActivity previewVideoActivity();
     @ContributesAndroidInjector abstract ReceiveExternalFilesActivity receiveExternalFilesActivity();
     @ContributesAndroidInjector abstract RequestCredentialsActivity requestCredentialsActivity();
-    @ContributesAndroidInjector abstract RichDocumentsWebView richDocumentsWebView();
     @ContributesAndroidInjector abstract SettingsActivity settingsActivity();
     @ContributesAndroidInjector abstract ShareActivity shareActivity();
     @ContributesAndroidInjector abstract SsoGrantPermissionActivity ssoGrantPermissionActivity();
@@ -126,6 +125,9 @@ abstract class ComponentsModule {
     @ContributesAndroidInjector abstract WhatsNewActivity whatsNewActivity();
     @ContributesAndroidInjector abstract EtmActivity etmActivity();
 
+    @ContributesAndroidInjector abstract RichDocumentsEditorWebView richDocumentsWebView();
+    @ContributesAndroidInjector abstract TextEditorWebView textEditorWebView();
+
     @ContributesAndroidInjector abstract ExtendedListFragment extendedListFragment();
     @ContributesAndroidInjector abstract FileDetailFragment fileDetailFragment();
     @ContributesAndroidInjector abstract LocalFileListFragment localFileListFragment();
@@ -137,7 +139,7 @@ abstract class ComponentsModule {
     @ContributesAndroidInjector abstract ContactListFragment chooseContactListFragment();
     @ContributesAndroidInjector abstract PreviewMediaFragment previewMediaFragment();
     @ContributesAndroidInjector abstract PreviewTextFragment previewTextFragment();
-
+    @ContributesAndroidInjector abstract PreviewTextStringFragment previewTextStringFragment();
     @ContributesAndroidInjector abstract PhotoFragment photoFragment();
 
     @ContributesAndroidInjector abstract MultipleAccountsDialog multipleAccountsDialog();

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

@@ -37,6 +37,9 @@ import androidx.annotation.NonNull;
  * Database provider for handling the persistence aspects of arbitrary data table.
  */
 public class ArbitraryDataProvider {
+    public static final String DIRECT_EDITING = "DIRECT_EDITING";
+    public static final String DIRECT_EDITING_ETAG = "DIRECT_EDITING_ETAG";
+
     private static final String TAG = ArbitraryDataProvider.class.getSimpleName();
     private static final String TRUE = "true";
 

+ 75 - 80
src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -215,6 +215,7 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.FILE_OWNER_DISPLAY_NAME, file.getOwnerDisplayName());
         cv.put(ProviderTableMeta.FILE_NOTE, file.getNote());
         cv.put(ProviderTableMeta.FILE_SHAREES, new Gson().toJson(file.getSharees()));
+        cv.put(ProviderTableMeta.FILE_RICH_WORKSPACE, file.getRichWorkspace());
 
         boolean sameRemotePath = fileExists(file.getRemotePath());
         if (sameRemotePath ||
@@ -465,6 +466,7 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.FILE_OWNER_DISPLAY_NAME, folder.getOwnerDisplayName());
         cv.put(ProviderTableMeta.FILE_NOTE, folder.getNote());
         cv.put(ProviderTableMeta.FILE_SHAREES, new Gson().toJson(folder.getSharees()));
+        cv.put(ProviderTableMeta.FILE_RICH_WORKSPACE, folder.getRichWorkspace());
 
         return cv;
     }
@@ -505,6 +507,7 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.FILE_OWNER_DISPLAY_NAME, file.getOwnerDisplayName());
         cv.put(ProviderTableMeta.FILE_NOTE, file.getNote());
         cv.put(ProviderTableMeta.FILE_SHAREES, new Gson().toJson(file.getSharees()));
+        cv.put(ProviderTableMeta.FILE_RICH_WORKSPACE, file.getRichWorkspace());
 
         return cv;
     }
@@ -1005,6 +1008,7 @@ public class FileDataStorageManager {
             file.setOwnerId(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_OWNER_ID)));
             file.setOwnerDisplayName(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_OWNER_DISPLAY_NAME)));
             file.setNote(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_NOTE)));
+            file.setRichWorkspace(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_RICH_WORKSPACE)));
 
             String sharees = c.getString(c.getColumnIndex(ProviderTableMeta.FILE_SHAREES));
 
@@ -2032,6 +2036,8 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_TEMPLATES, capability.getRichDocumentsTemplatesAvailable()
             .getValue());
         cv.put(ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_PRODUCT_NAME, capability.getRichDocumentsProductName());
+        cv.put(ProviderTableMeta.CAPABILITIES_DIRECT_EDITING_ETAG, capability.getDirectEditingEtag());
+
         return cv;
     }
 
@@ -2086,97 +2092,71 @@ public class FileDataStorageManager {
         OCCapability capability = null;
         if (c != null) {
             capability = new OCCapability();
-            capability.setId(c.getLong(c.getColumnIndex(ProviderTableMeta._ID)));
-            capability.setAccountName(c.getString(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_ACCOUNT_NAME)));
-            capability.setVersionMayor(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_VERSION_MAYOR)));
-            capability.setVersionMinor(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_VERSION_MINOR)));
-            capability.setVersionMicro(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_VERSION_MICRO)));
-            capability.setVersionString(c.getString(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_VERSION_STRING)));
-            capability.setVersionEdition(c.getString(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_VERSION_EDITION)));
-            capability.setExtendedSupport(CapabilityBooleanType.fromValue(c.getInt(c
-                                                                                       .getColumnIndex(ProviderTableMeta.CAPABILITIES_EXTENDED_SUPPORT))));
-            capability.setCorePollInterval(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_CORE_POLLINTERVAL)));
-            capability.setFilesSharingApiEnabled(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_SHARING_API_ENABLED))));
-            capability.setFilesSharingPublicEnabled(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_ENABLED))));
-            capability.setFilesSharingPublicPasswordEnforced(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED))));
+            capability.setId(getLong(c, ProviderTableMeta._ID));
+            capability.setAccountName(getString(c, ProviderTableMeta.CAPABILITIES_ACCOUNT_NAME));
+            capability.setVersionMayor(getInt(c, ProviderTableMeta.CAPABILITIES_VERSION_MAYOR));
+            capability.setVersionMinor(getInt(c, ProviderTableMeta.CAPABILITIES_VERSION_MINOR));
+            capability.setVersionMicro(getInt(c, ProviderTableMeta.CAPABILITIES_VERSION_MICRO));
+            capability.setVersionString(getString(c, ProviderTableMeta.CAPABILITIES_VERSION_STRING));
+            capability.setVersionEdition(getString(c, ProviderTableMeta.CAPABILITIES_VERSION_EDITION));
+            capability.setExtendedSupport(getBoolean(c, ProviderTableMeta.CAPABILITIES_EXTENDED_SUPPORT));
+            capability.setCorePollInterval(getInt(c, ProviderTableMeta.CAPABILITIES_CORE_POLLINTERVAL));
+            capability.setFilesSharingApiEnabled(getBoolean(c, ProviderTableMeta.CAPABILITIES_SHARING_API_ENABLED));
+            capability.setFilesSharingPublicEnabled(
+                getBoolean(c, ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_ENABLED));
+            capability.setFilesSharingPublicPasswordEnforced(
+                getBoolean(c, ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED));
             capability.setFilesSharingPublicAskForOptionalPassword(
-                CapabilityBooleanType.fromValue(
-                    c.getInt(c.getColumnIndex(ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_ASK_FOR_OPTIONAL_PASSWORD))));
-            capability.setFilesSharingPublicExpireDateEnabled(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED))));
-            capability.setFilesSharingPublicExpireDateDays(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS)));
-            capability.setFilesSharingPublicExpireDateEnforced(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED))));
-            capability.setFilesSharingPublicSendMail(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_SEND_MAIL))));
-            capability.setFilesSharingPublicUpload(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_UPLOAD))));
-            capability.setFilesSharingUserSendMail(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_SHARING_USER_SEND_MAIL))));
-            capability.setFilesSharingResharing(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_SHARING_RESHARING))));
-            capability.setFilesSharingFederationOutgoing(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_SHARING_FEDERATION_OUTGOING))));
-            capability.setFilesSharingFederationIncoming(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_SHARING_FEDERATION_INCOMING))));
-            capability.setFilesBigFileChunking(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_FILES_BIGFILECHUNKING))));
-            capability.setFilesUndelete(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_FILES_UNDELETE))));
-            capability.setFilesVersioning(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_FILES_VERSIONING))));
-            capability.setFilesFileDrop(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_FILES_DROP))));
-            capability.setExternalLinks(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_EXTERNAL_LINKS))));
-            capability.setServerName(c.getString(c.getColumnIndex(ProviderTableMeta.CAPABILITIES_SERVER_NAME)));
-            capability.setServerColor(c.getString(c.getColumnIndex(ProviderTableMeta.CAPABILITIES_SERVER_COLOR)));
-            capability.setServerTextColor(
-                    c.getString(c.getColumnIndex(ProviderTableMeta.CAPABILITIES_SERVER_TEXT_COLOR)));
-            capability.setServerElementColor(
-                    c.getString(c.getColumnIndex(ProviderTableMeta.CAPABILITIES_SERVER_ELEMENT_COLOR)));
-            capability.setServerBackground(c.getString(c.getColumnIndex(
-                    ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_URL)));
-            capability.setServerSlogan(c.getString(c.getColumnIndex(ProviderTableMeta.CAPABILITIES_SERVER_SLOGAN)));
-            capability.setEndToEndEncryption(CapabilityBooleanType.fromValue(c.getInt(c
-                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION))));
-            capability.setServerBackgroundDefault(CapabilityBooleanType.fromValue(
-                    c.getInt(c.getColumnIndex(ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_DEFAULT))));
-            capability.setServerBackgroundPlain(CapabilityBooleanType.fromValue(
-                    c.getInt(c.getColumnIndex(ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_PLAIN))));
-            capability.setActivity(CapabilityBooleanType.fromValue(
-                    c.getInt(c.getColumnIndex(ProviderTableMeta.CAPABILITIES_ACTIVITY))));
-            capability.setRichDocuments(CapabilityBooleanType.fromValue(c.getInt(
-                    c.getColumnIndex(ProviderTableMeta.CAPABILITIES_RICHDOCUMENT))));
-            capability.setRichDocumentsDirectEditing(CapabilityBooleanType.fromValue(c.getInt(
-                c.getColumnIndex(ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_DIRECT_EDITING))));
-            capability.setRichDocumentsTemplatesAvailable(CapabilityBooleanType.fromValue(c.getInt(
-                c.getColumnIndex(ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_TEMPLATES))));
+                getBoolean(c, ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_ASK_FOR_OPTIONAL_PASSWORD));
+            capability.setFilesSharingPublicExpireDateEnabled(
+                getBoolean(c, ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED));
+            capability.setFilesSharingPublicExpireDateDays(
+                getInt(c, ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS));
+            capability.setFilesSharingPublicExpireDateEnforced(
+                getBoolean(c, ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED));
+            capability.setFilesSharingPublicSendMail(
+                getBoolean(c, ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_SEND_MAIL));
+            capability.setFilesSharingPublicUpload(getBoolean(c, ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_UPLOAD));
+            capability.setFilesSharingUserSendMail(getBoolean(c, ProviderTableMeta.CAPABILITIES_SHARING_USER_SEND_MAIL));
+            capability.setFilesSharingResharing(getBoolean(c, ProviderTableMeta.CAPABILITIES_SHARING_RESHARING));
+            capability.setFilesSharingFederationOutgoing(
+                getBoolean(c, ProviderTableMeta.CAPABILITIES_SHARING_FEDERATION_OUTGOING));
+            capability.setFilesSharingFederationIncoming(
+                getBoolean(c, ProviderTableMeta.CAPABILITIES_SHARING_FEDERATION_INCOMING));
+            capability.setFilesBigFileChunking(getBoolean(c, ProviderTableMeta.CAPABILITIES_FILES_BIGFILECHUNKING));
+            capability.setFilesUndelete(getBoolean(c, ProviderTableMeta.CAPABILITIES_FILES_UNDELETE));
+            capability.setFilesVersioning(getBoolean(c, ProviderTableMeta.CAPABILITIES_FILES_VERSIONING));
+            capability.setFilesFileDrop(getBoolean(c, ProviderTableMeta.CAPABILITIES_FILES_DROP));
+            capability.setExternalLinks(getBoolean(c, ProviderTableMeta.CAPABILITIES_EXTERNAL_LINKS));
+            capability.setServerName(getString(c, ProviderTableMeta.CAPABILITIES_SERVER_NAME));
+            capability.setServerColor(getString(c, ProviderTableMeta.CAPABILITIES_SERVER_COLOR));
+            capability.setServerTextColor(getString(c, ProviderTableMeta.CAPABILITIES_SERVER_TEXT_COLOR));
+            capability.setServerElementColor(getString(c, ProviderTableMeta.CAPABILITIES_SERVER_ELEMENT_COLOR));
+            capability.setServerBackground(getString(c, ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_URL));
+            capability.setServerSlogan(getString(c, ProviderTableMeta.CAPABILITIES_SERVER_SLOGAN));
+            capability.setEndToEndEncryption(getBoolean(c, ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION));
+            capability.setServerBackgroundDefault(
+                getBoolean(c, ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_DEFAULT));
+            capability.setServerBackgroundPlain(getBoolean(c, ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_PLAIN));
+            capability.setActivity(getBoolean(c, ProviderTableMeta.CAPABILITIES_ACTIVITY));
+            capability.setRichDocuments(getBoolean(c, ProviderTableMeta.CAPABILITIES_RICHDOCUMENT));
+            capability.setRichDocumentsDirectEditing(
+                getBoolean(c, ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_DIRECT_EDITING));
+            capability.setRichDocumentsTemplatesAvailable(
+                getBoolean(c, ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_TEMPLATES));
             String mimetypes = c.getString(c.getColumnIndex(ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_MIMETYPE_LIST));
             if (mimetypes == null) {
                 mimetypes = "";
             }
             capability.setRichDocumentsMimeTypeList(Arrays.asList(mimetypes.split(",")));
 
-            String optionalMimetypes = c.getString(c.getColumnIndex(
-                ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_OPTIONAL_MIMETYPE_LIST));
+            String optionalMimetypes = getString(c, ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_OPTIONAL_MIMETYPE_LIST);
             if (optionalMimetypes == null) {
                 optionalMimetypes = "";
             }
             capability.setRichDocumentsOptionalMimeTypeList(Arrays.asList(optionalMimetypes.split(",")));
-            capability.setRichDocumentsProductName(
-                c.getString(c.getColumnIndex(ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_PRODUCT_NAME)));
+            capability.setRichDocumentsProductName(getString(c, ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_PRODUCT_NAME));
+            capability.setDirectEditingEtag(getString(c, ProviderTableMeta.CAPABILITIES_DIRECT_EDITING_ETAG));
         }
         return capability;
     }
@@ -2299,4 +2279,19 @@ public class FileDataStorageManager {
         }
     }
 
+    private String getString(Cursor cursor, String columnName) {
+        return cursor.getString(cursor.getColumnIndex(columnName));
+    }
+
+    private int getInt(Cursor cursor, String columnName) {
+        return cursor.getInt(cursor.getColumnIndex(columnName));
+    }
+
+    private long getLong(Cursor cursor, String columnName) {
+        return cursor.getLong(cursor.getColumnIndex(columnName));
+    }
+
+    private CapabilityBooleanType getBoolean(Cursor cursor, String columnName) {
+        return CapabilityBooleanType.fromValue(cursor.getInt(cursor.getColumnIndex(columnName)));
+    }
 }

+ 4 - 0
src/main/java/com/owncloud/android/datamodel/OCFile.java

@@ -91,6 +91,7 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
     @Getter @Setter private String ownerDisplayName;
     @Getter @Setter String note;
     @Getter @Setter private List<ShareeUser> sharees;
+    @Getter @Setter private String richWorkspace;
 
     /**
      * URI to the local path of the file contents, if stored in the device; cached after first call
@@ -158,6 +159,7 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
         ownerId = source.readString();
         ownerDisplayName = source.readString();
         mountType = (WebdavEntry.MountType) source.readSerializable();
+        richWorkspace = source.readString();
     }
 
     @Override
@@ -190,6 +192,7 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
         dest.writeString(ownerId);
         dest.writeString(ownerDisplayName);
         dest.writeSerializable(mountType);
+        dest.writeString(richWorkspace);
     }
 
     public String getDecryptedRemotePath() {
@@ -408,6 +411,7 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
         encrypted = false;
         encryptedFileName = null;
         mountType = WebdavEntry.MountType.INTERNAL;
+        richWorkspace = "";
     }
 
     /**

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

@@ -37,10 +37,13 @@ import lombok.Setter;
 @AllArgsConstructor
 @NoArgsConstructor
 public class Template {
+    public enum Type {
+        DOCUMENT, SPREADSHEET, PRESENTATION
+    }
 
     public int id;
     public String name;
     public String thumbnailLink;
-    public String type;
+    public Type type;
     public String extension;
 }

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

@@ -31,7 +31,7 @@ import com.owncloud.android.MainApp;
  */
 public class ProviderMeta {
     public static final String DB_NAME = "filelist";
-    public static final int DB_VERSION = 51;
+    public static final int DB_VERSION = 52;
 
     private ProviderMeta() {
         // No instance
@@ -47,6 +47,8 @@ public class ProviderMeta {
         public static final String ARBITRARY_DATA_TABLE_NAME = "arbitrary_data";
         public static final String VIRTUAL_TABLE_NAME = "virtual";
         public static final String FILESYSTEM_TABLE_NAME = "filesystem";
+        public static final String EDITORS_TABLE_NAME = "editors";
+        public static final String CREATORS_TABLE_NAME = "creators";
 
         private static final String CONTENT_PREFIX = "content://";
 
@@ -110,6 +112,7 @@ public class ProviderMeta {
         public static final String FILE_OWNER_DISPLAY_NAME = "owner_display_name";
         public static final String FILE_NOTE = "note";
         public static final String FILE_SHAREES = "sharees";
+        public static final String FILE_RICH_WORKSPACE = "rich_workspace";
 
         public static final String[] FILE_ALL_COLUMNS = {
             _ID, FILE_PARENT, FILE_NAME, FILE_CREATION, FILE_MODIFIED,
@@ -117,7 +120,8 @@ public class ProviderMeta {
             FILE_PATH, FILE_ACCOUNT_OWNER, FILE_LAST_SYNC_DATE, FILE_LAST_SYNC_DATE_FOR_DATA, FILE_ETAG,
             FILE_ETAG_ON_SERVER, FILE_SHARED_VIA_LINK, FILE_SHARED_WITH_SHAREE, FILE_PUBLIC_LINK, FILE_PERMISSIONS,
             FILE_REMOTE_ID, FILE_UPDATE_THUMBNAIL, FILE_IS_DOWNLOADING, FILE_ETAG_IN_CONFLICT, FILE_FAVORITE,
-            FILE_IS_ENCRYPTED, FILE_MOUNT_TYPE, FILE_HAS_PREVIEW, FILE_UNREAD_COMMENTS_COUNT, FILE_SHAREES
+            FILE_IS_ENCRYPTED, FILE_MOUNT_TYPE, FILE_HAS_PREVIEW, FILE_UNREAD_COMMENTS_COUNT, FILE_SHAREES,
+            FILE_RICH_WORKSPACE
         };
 
         public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME + " collate nocase asc";
@@ -192,9 +196,9 @@ public class ProviderMeta {
         public static final String CAPABILITIES_RICHDOCUMENT_DIRECT_EDITING = "richdocument_direct_editing";
         public static final String CAPABILITIES_RICHDOCUMENT_TEMPLATES = "richdocument_direct_templates";
         public static final String CAPABILITIES_RICHDOCUMENT_PRODUCT_NAME = "richdocument_product_name";
-
         public static final String CAPABILITIES_DEFAULT_SORT_ORDER = CAPABILITIES_ACCOUNT_NAME
                 + " collate nocase asc";
+        public static final String CAPABILITIES_DIRECT_EDITING_ETAG = "direct_editing_etag";
 
         //Columns of Uploads table
         public static final String UPLOADS_LOCAL_PATH = "local_path";

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

@@ -79,10 +79,10 @@ public class FetchTemplateOperation extends RemoteOperation {
                     JSONObject templateObject = templates.getJSONObject(i);
 
                     templateArray.add(new Template(templateObject.getInt("id"),
-                        templateObject.getString("name"),
-                        templateObject.optString("preview"),
-                        templateObject.getString("type"),
-                        templateObject.getString("extension")));
+                                                   templateObject.getString("name"),
+                                                   templateObject.optString("preview"),
+                                                   Template.Type.valueOf(templateObject.getString("type")),
+                                                   templateObject.getString("extension")));
                 }
 
                 result = new RemoteOperationResult(true, getMethod);

+ 56 - 19
src/main/java/com/owncloud/android/files/FileMenuFilter.java

@@ -22,19 +22,25 @@
 package com.owncloud.android.files;
 
 import android.accounts.Account;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.os.Build;
 import android.view.Menu;
 import android.view.MenuItem;
 
+import com.google.gson.Gson;
 import com.owncloud.android.R;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
+import com.owncloud.android.lib.common.DirectEditing;
+import com.owncloud.android.lib.common.Editor;
 import com.owncloud.android.lib.resources.status.OCCapability;
 import com.owncloud.android.services.OperationsService.OperationsServiceBinder;
 import com.owncloud.android.ui.activity.ComponentsGetter;
-import com.owncloud.android.ui.activity.RichDocumentsWebView;
 import com.owncloud.android.utils.MimeTypeUtil;
+import com.owncloud.android.utils.NextcloudServer;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -42,6 +48,8 @@ import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 
+import androidx.annotation.Nullable;
+
 /**
  * Filters out the file actions available in a given {@link Menu} for a given {@link OCFile}
  * according to the current state of the latest.
@@ -172,6 +180,7 @@ public class FileMenuFilter {
         OCCapability capability = mComponentsGetter.getStorageManager().getCapability(mAccount.name);
         boolean endToEndEncryptionEnabled = capability.getEndToEndEncryption().isTrue();
 
+        filterEdit(toShow, toHide, capability);
         filterDownload(toShow, toHide, synchronizing);
         filterRename(toShow, toHide, synchronizing);
         filterMoveCopy(toShow, toHide, synchronizing);
@@ -189,7 +198,6 @@ public class FileMenuFilter {
         filterUnsetEncrypted(toShow, toHide, endToEndEncryptionEnabled);
         filterSetPictureAs(toShow, toHide);
         filterStream(toShow, toHide, isMediaSupported);
-        filterOpenAsRichDocument(toShow, toHide, capability, menu);
     }
 
     private void filterShareFile(List<Integer> toShow, List<Integer> toHide, OCCapability capability) {
@@ -252,29 +260,58 @@ public class FileMenuFilter {
         }
     }
 
-    private void filterOpenAsRichDocument(List<Integer> toShow,
-                                          List<Integer> toHide,
-                                          OCCapability capability,
-                                          Menu menu) {
+    private void filterEdit(List<Integer> toShow,
+                            List<Integer> toHide,
+                            OCCapability capability
+    ) {
+        if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+            toHide.add(R.id.action_edit);
+            return;
+        }
+
         String mimeType = mFiles.iterator().next().getMimeType();
 
-        if (isSingleFile() && android.os.Build.VERSION.SDK_INT >= RichDocumentsWebView.MINIMUM_API &&
-            (capability.getRichDocumentsMimeTypeList().contains(mimeType) ||
-                capability.getRichDocumentsOptionalMimeTypeList().contains(mimeType)) &&
-            capability.getRichDocumentsDirectEditing().isTrue()) {
+        if (isRichDocumentEditingSupported(capability, mimeType) || isEditorAvailable(mContext.getContentResolver(),
+                                                                                      mAccount,
+                                                                                      mimeType)) {
+            toShow.add(R.id.action_edit);
+        } else {
+            toHide.add(R.id.action_edit);
+        }
+    }
 
-            String openWith = mContext.getResources().getString(R.string.actionbar_open_as_richdocument_parameter);
-            String productName = capability.getRichDocumentsProductName();
-            MenuItem item = menu.findItem(R.id.action_open_file_as_richdocument);
+    public static boolean isEditorAvailable(ContentResolver contentResolver, Account account, String mimeType) {
+        return getEditor(contentResolver, account, mimeType) != null;
+    }
 
-            if (item != null) {
-                item.setTitle(String.format(openWith, productName));
-            }
+    @Nullable
+    public static Editor getEditor(ContentResolver contentResolver, Account account, String mimeType) {
+        String json = new ArbitraryDataProvider(contentResolver).getValue(account, ArbitraryDataProvider.DIRECT_EDITING);
 
-            toShow.add(R.id.action_open_file_as_richdocument);
-        } else {
-            toHide.add(R.id.action_open_file_as_richdocument);
+        if (json.isEmpty()) {
+            return null;
         }
+
+        DirectEditing directEditing = new Gson().fromJson(json, DirectEditing.class);
+
+        for (Editor editor : directEditing.editors.values()) {
+            if (editor.mimetypes.contains(mimeType) || editor.optionalMimetypes.contains(mimeType)) {
+                return editor;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * This will be replaced by unified editor and can be removed once EOL of corresponding server version.
+     */
+    @NextcloudServer(max = 18)
+    private boolean isRichDocumentEditingSupported(OCCapability capability, String mimeType) {
+        return isSingleFile() && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
+            (capability.getRichDocumentsMimeTypeList().contains(mimeType) ||
+                capability.getRichDocumentsOptionalMimeTypeList().contains(mimeType)) &&
+            capability.getRichDocumentsDirectEditing().isTrue();
     }
 
     private void filterSync(List<Integer> toShow, List<Integer> toHide, boolean synchronizing) {

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

@@ -24,9 +24,13 @@ import android.content.Context;
 import android.content.Intent;
 import android.util.Log;
 
+import com.google.gson.Gson;
+import com.nextcloud.android.lib.resources.directediting.DirectEditingObtainRemoteOperation;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.DecryptedFolderMetadata;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.lib.common.DirectEditing;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
@@ -275,13 +279,39 @@ public class RefreshFolderOperation extends RemoteOperation {
     }
 
     private void updateCapabilities() {
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(mContext.getContentResolver());
+        String oldDirectEditingEtag = arbitraryDataProvider.getValue(mAccount,
+                                                                     ArbitraryDataProvider.DIRECT_EDITING_ETAG);
+
         GetCapabilitiesOperation getCapabilities = new GetCapabilitiesOperation();
         RemoteOperationResult result = getCapabilities.execute(mStorageManager, mContext);
-        if (!result.isSuccess()) {
+        if (result.isSuccess()) {
+            String newDirectEditingEtag = mStorageManager.getCapability(mAccount.name).getDirectEditingEtag();
+
+            if (!oldDirectEditingEtag.equalsIgnoreCase(newDirectEditingEtag)) {
+                updateDirectEditing(arbitraryDataProvider, newDirectEditingEtag);
+            }
+        } else {
             Log_OC.w(TAG, "Update Capabilities unsuccessfully");
         }
     }
 
+    private void updateDirectEditing(ArbitraryDataProvider arbitraryDataProvider, String newDirectEditingEtag) {
+        RemoteOperationResult result = new DirectEditingObtainRemoteOperation().execute(mAccount, mContext);
+
+        if (result.isSuccess()) {
+            DirectEditing directEditing = (DirectEditing) result.getSingleData();
+            String json = new Gson().toJson(directEditing);
+            arbitraryDataProvider.storeOrUpdateKeyValue(mAccount.name, ArbitraryDataProvider.DIRECT_EDITING, json);
+        } else {
+            arbitraryDataProvider.deleteKeyForAccount(mAccount.name, ArbitraryDataProvider.DIRECT_EDITING);
+        }
+
+        arbitraryDataProvider.storeOrUpdateKeyValue(mAccount.name,
+                                                    ArbitraryDataProvider.DIRECT_EDITING_ETAG,
+                                                    newDirectEditingEtag);
+    }
+
     private RemoteOperationResult checkForChanges(OwnCloudClient client) {
         mRemoteFolderChanged = true;
         RemoteOperationResult result;
@@ -392,6 +422,9 @@ public class RefreshFolderOperation extends RemoteOperation {
         // update permission
         mLocalFolder.setPermissions(remoteFolder.getPermissions());
 
+        // update richWorkpace
+        mLocalFolder.setRichWorkspace(remoteFolder.getRichWorkspace());
+
         DecryptedFolderMetadata metadata = getDecryptedFolderMetadata(encryptedAncestor);
 
         // get current data about local contents of the folder to synchronize

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

@@ -24,6 +24,7 @@ import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.utils.NextcloudServer;
 
 import org.apache.commons.httpclient.HttpStatus;
 import org.apache.commons.httpclient.methods.PostMethod;
@@ -33,6 +34,11 @@ import org.json.JSONObject;
  * Edit a file with Richdocuments. Returns URL which can be shown in WebView.
  */
 public class RichDocumentsUrlOperation extends RemoteOperation {
+
+    /**
+     * TODO move to library
+     */
+
     private static final String TAG = RichDocumentsUrlOperation.class.getSimpleName();
     private static final int SYNC_READ_TIMEOUT = 40000;
     private static final int SYNC_CONNECTION_TIMEOUT = 5000;
@@ -51,6 +57,7 @@ public class RichDocumentsUrlOperation extends RemoteOperation {
         this.fileID = fileID;
     }
 
+    @NextcloudServer(max = 18)
     protected RemoteOperationResult run(OwnCloudClient client) {
         RemoteOperationResult result;
         PostMethod postMethod = null;
@@ -80,7 +87,7 @@ public class RichDocumentsUrlOperation extends RemoteOperation {
         } catch (Exception e) {
             result = new RemoteOperationResult(e);
             Log_OC.e(TAG, "Get rich document url for file with id " + fileID + " failed: " + result.getLogMessage(),
-                    result.getException());
+                     result.getException());
         } finally {
             if (postMethod != null) {
                 postMethod.releaseConnection();

+ 12 - 3
src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java

@@ -53,6 +53,7 @@ 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.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
@@ -163,7 +164,11 @@ public class DocumentsStorageProvider extends DocumentsProvider {
 
         boolean isLoading = false;
         if (parentFolder.isExpired()) {
-            final ReloadFolderDocumentTask task = new ReloadFolderDocumentTask(parentFolder, result -> {
+            ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(getContext().getContentResolver());
+
+            final ReloadFolderDocumentTask task = new ReloadFolderDocumentTask(arbitraryDataProvider,
+                                                                               parentFolder,
+                                                                               result -> {
                 getContext().getContentResolver().notifyChange(toNotifyUri(parentFolder), null, false);
             });
             task.executeOnExecutor(executor);
@@ -309,7 +314,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
         }
 
         Document document = toDocument(documentId);
-        
+
         boolean exists = ThumbnailsCacheManager.containsBitmap(ThumbnailsCacheManager.PREFIX_THUMBNAIL
                                                                    + document.getFile().getRemoteId());
 
@@ -692,10 +697,14 @@ public class DocumentsStorageProvider extends DocumentsProvider {
 
         private final Document folder;
         private final OnTaskFinishedCallback callback;
+        private final ArbitraryDataProvider arbitraryDataProvider;
 
-        ReloadFolderDocumentTask(Document folder, OnTaskFinishedCallback callback) {
+        ReloadFolderDocumentTask(ArbitraryDataProvider arbitraryDataProvider,
+                                 Document folder,
+                                 OnTaskFinishedCallback callback) {
             this.folder = folder;
             this.callback = callback;
+            this.arbitraryDataProvider = arbitraryDataProvider;
         }
 
         @Override

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

@@ -719,7 +719,8 @@ public class FileContentProvider extends ContentProvider {
                        + ProviderTableMeta.FILE_OWNER_ID + TEXT
                        + ProviderTableMeta.FILE_OWNER_DISPLAY_NAME + TEXT
                        + ProviderTableMeta.FILE_NOTE + TEXT
-                       + ProviderTableMeta.FILE_SHAREES + " TEXT);"
+                       + ProviderTableMeta.FILE_SHAREES + TEXT
+                       + ProviderTableMeta.FILE_RICH_WORKSPACE + " TEXT);"
         );
     }
 
@@ -789,7 +790,8 @@ public class FileContentProvider extends ContentProvider {
                        + ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_TEMPLATES + INTEGER
                        + ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_OPTIONAL_MIMETYPE_LIST + TEXT
                        + ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_ASK_FOR_OPTIONAL_PASSWORD + INTEGER
-                       + ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_PRODUCT_NAME + " TEXT );");
+                       + ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_PRODUCT_NAME + TEXT
+                       + ProviderTableMeta.CAPABILITIES_DIRECT_EDITING_ETAG + " TEXT );");
     }
 
     private void createUploadsTable(SQLiteDatabase db) {
@@ -2064,6 +2066,42 @@ public class FileContentProvider extends ContentProvider {
             if (!upgraded) {
                 Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
             }
+
+            if (oldVersion < 52 && newVersion >= 52) {
+                Log_OC.i(SQL, "Entering in the #52 add etag for directEditing to capability");
+                db.beginTransaction();
+                try {
+                    db.execSQL(ALTER_TABLE + ProviderTableMeta.CAPABILITIES_TABLE_NAME +
+                                   ADD_COLUMN + ProviderTableMeta.CAPABILITIES_DIRECT_EDITING_ETAG + " TEXT ");
+
+                    upgraded = true;
+                    db.setTransactionSuccessful();
+                } finally {
+                    db.endTransaction();
+                }
+            }
+
+            if (!upgraded) {
+                Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
+            }
+
+            if (oldVersion < 52 && newVersion >= 52) {
+                Log_OC.i(SQL, "Entering in the #52 add rich workspace to file table");
+                db.beginTransaction();
+                try {
+                    db.execSQL(ALTER_TABLE + ProviderTableMeta.FILE_TABLE_NAME +
+                                   ADD_COLUMN + ProviderTableMeta.FILE_RICH_WORKSPACE + " TEXT ");
+
+                    upgraded = true;
+                    db.setTransactionSuccessful();
+                } finally {
+                    db.endTransaction();
+                }
+            }
+
+            if (!upgraded) {
+                Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
+            }
         }
 
         @Override

+ 228 - 0
src/main/java/com/owncloud/android/ui/activity/EditorWebView.java

@@ -0,0 +1,228 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2019 Tobias Kaminsky
+ * Copyright (C) 2019 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.activity;
+
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.view.View;
+import android.webkit.JavascriptInterface;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.google.android.material.snackbar.Snackbar;
+import com.owncloud.android.R;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.datamodel.ThumbnailsCacheManager;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.ui.asynctasks.LoadUrlTask;
+import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.MimeTypeUtil;
+import com.owncloud.android.utils.ThemeUtils;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.Unbinder;
+import lombok.Getter;
+import lombok.Setter;
+
+public abstract class EditorWebView extends ExternalSiteWebView {
+    @Getter @Setter protected Snackbar loadingSnackbar;
+    protected OCFile file;
+
+    @BindView(R.id.progressBar2)
+    ProgressBar progressBar;
+
+    @BindView(R.id.thumbnail)
+    ImageView thumbnail;
+
+    @BindView(R.id.filename)
+    TextView fileName;
+
+    private Unbinder unbinder;
+
+    private static final String TAG = EditorWebView.class.getSimpleName();
+
+    protected void loadUrl(String url, OCFile file) {
+        if (TextUtils.isEmpty(url)) {
+            new LoadUrlTask(this, getAccount(), file).execute();
+        } else {
+            webview.loadUrl(url);
+        }
+    }
+
+    protected void hideLoading() {
+        thumbnail.setVisibility(View.GONE);
+        fileName.setVisibility(View.GONE);
+        progressBar.setVisibility(View.GONE);
+        webview.setVisibility(View.VISIBLE);
+
+        if (loadingSnackbar != null) {
+            loadingSnackbar.dismiss();
+        }
+    }
+
+    public void onUrlLoaded(String loadedUrl) {
+        this.url = loadedUrl;
+
+        if (!url.isEmpty()) {
+            getWebview().loadUrl(url);
+
+            new Handler().postDelayed(() -> {
+                if (getWebview().getVisibility() != View.VISIBLE) {
+                    Snackbar snackbar = DisplayUtils.createSnackbar(findViewById(android.R.id.content),
+                                                                    R.string.timeout_richDocuments, Snackbar.LENGTH_INDEFINITE)
+                        .setAction(R.string.fallback_weblogin_back, v -> hideLoading());
+
+                    ThemeUtils.colorSnackbar(getApplicationContext(), snackbar);
+                    setLoadingSnackbar(snackbar);
+                    snackbar.show();
+                }
+            }, 10 * 1000);
+        } else {
+            Toast.makeText(getApplicationContext(),
+                           R.string.richdocuments_failed_to_load_document, Toast.LENGTH_LONG).show();
+            finish();
+        }
+    }
+
+    public void closeView() {
+        webview.destroy();
+        finish();
+    }
+
+    @SuppressLint("AddJavascriptInterface") // suppress warning as webview is only used >= Lollipop
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        webViewLayout = R.layout.richdocuments_webview; // TODO rename
+
+        showToolbar = false;
+
+        super.onCreate(savedInstanceState);
+
+        unbinder = ButterKnife.bind(this);
+
+        file = getIntent().getParcelableExtra(ExternalSiteWebView.EXTRA_FILE);
+
+        initLoadingScreen();
+    }
+
+    protected void initLoadingScreen() {
+        setThumbnail(file, thumbnail);
+        fileName.setText(file.getFileName());
+    }
+
+    private void openShareDialog() {
+        Intent intent = new Intent(this, ShareActivity.class);
+        intent.putExtra(FileActivity.EXTRA_FILE, file);
+        intent.putExtra(FileActivity.EXTRA_ACCOUNT, getAccount());
+        startActivity(intent);
+    }
+
+    @Override
+    protected void onDestroy() {
+        unbinder.unbind();
+        webview.destroy();
+
+        super.onDestroy();
+    }
+
+    protected void setThumbnail(OCFile file, ImageView thumbnailView) {
+        // Todo minimize: only icon by mimetype
+
+        if (file.isFolder()) {
+            thumbnailView.setImageDrawable(MimeTypeUtil.getFolderTypeIcon(file.isSharedWithMe() ||
+                                                                              file.isSharedWithSharee(), file.isSharedViaLink(), file.isEncrypted(), file.getMountType(),
+                                                                          this));
+        } else {
+            if ((MimeTypeUtil.isImage(file) || MimeTypeUtil.isVideo(file)) && file.getRemoteId() != null) {
+                // Thumbnail in cache?
+                Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
+                    ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId());
+
+                if (thumbnail != null && !file.isUpdateThumbnailNeeded()) {
+                    if (MimeTypeUtil.isVideo(file)) {
+                        Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail);
+                        thumbnailView.setImageBitmap(withOverlay);
+                    } else {
+                        thumbnailView.setImageBitmap(thumbnail);
+                    }
+                } else {
+                    // generate new thumbnail
+                    if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView)) {
+                        try {
+                            final ThumbnailsCacheManager.ThumbnailGenerationTask task =
+                                new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView,
+                                                                                   getStorageManager(), getAccount());
+
+                            if (thumbnail == null) {
+                                if (MimeTypeUtil.isVideo(file)) {
+                                    thumbnail = ThumbnailsCacheManager.mDefaultVideo;
+                                } else {
+                                    thumbnail = ThumbnailsCacheManager.mDefaultImg;
+                                }
+                            }
+                            final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable =
+                                new ThumbnailsCacheManager.AsyncThumbnailDrawable(getResources(), thumbnail, task);
+                            thumbnailView.setImageDrawable(asyncDrawable);
+                            task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file,
+                                                                                                  file.getRemoteId()));
+                        } catch (IllegalArgumentException e) {
+                            Log_OC.d(TAG, "ThumbnailGenerationTask : " + e.getMessage());
+                        }
+                    }
+                }
+
+                if ("image/png".equalsIgnoreCase(file.getMimeType())) {
+                    thumbnailView.setBackgroundColor(getResources().getColor(R.color.bg_default));
+                }
+            } else {
+                thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimeType(), file.getFileName(),
+                                                                            getAccount(), this));
+            }
+        }
+    }
+
+    public class MobileInterface {
+        @JavascriptInterface
+        public void close() {
+            runOnUiThread(EditorWebView.this::closeView);
+        }
+
+        @JavascriptInterface
+        public void share() {
+            openShareDialog();
+        }
+
+        @JavascriptInterface
+        public void loaded() {
+            runOnUiThread(EditorWebView.this::hideLoading);
+        }
+    }
+
+}

+ 7 - 0
src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java

@@ -91,6 +91,7 @@ public class ExternalSiteWebView extends FileActivity {
         webview.setFocusable(true);
         webview.setFocusableInTouchMode(true);
         webview.setClickable(true);
+//        webview.addJavascriptInterface(new TestMobileInterface(), "RichDocumentsMobileInterface");
 
         // allow debugging (when building the debug version); see details in
         // https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews
@@ -163,6 +164,12 @@ public class ExternalSiteWebView extends FileActivity {
         // enable javascript
         webSettings.setJavaScriptEnabled(true);
         webSettings.setDomStorageEnabled(true);
+
+        // caching disabled in debug mode
+        if ((getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) == 0) {
+            webSettings.setAppCacheEnabled(true);
+            webSettings.setAppCachePath(getCacheDir().getPath());
+        }
     }
 
     private void setupActionBar(String title) {

+ 26 - 7
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -113,7 +113,9 @@ import com.owncloud.android.ui.helpers.UriUploader;
 import com.owncloud.android.ui.preview.PreviewImageActivity;
 import com.owncloud.android.ui.preview.PreviewImageFragment;
 import com.owncloud.android.ui.preview.PreviewMediaFragment;
+import com.owncloud.android.ui.preview.PreviewTextFileFragment;
 import com.owncloud.android.ui.preview.PreviewTextFragment;
+import com.owncloud.android.ui.preview.PreviewTextStringFragment;
 import com.owncloud.android.ui.preview.PreviewVideoActivity;
 import com.owncloud.android.utils.DataHolderUtil;
 import com.owncloud.android.utils.DisplayUtils;
@@ -478,7 +480,7 @@ public class FileDisplayActivity extends FileActivity
                     cleanSecondFragment();
                     if (file.isDown() && MimeTypeUtil.isVCard(file.getMimeType())) {
                         startContactListFragment(file);
-                    } else if (file.isDown() && PreviewTextFragment.canBePreviewed(file)) {
+                    } else if (file.isDown() && PreviewTextFileFragment.canBePreviewed(file)) {
                         startTextPreview(file, false);
                     }
                 }
@@ -597,7 +599,7 @@ public class FileDisplayActivity extends FileActivity
                 int startPlaybackPosition = getIntent().getIntExtra(PreviewVideoActivity.EXTRA_START_POSITION, 0);
                 boolean autoplay = getIntent().getBooleanExtra(PreviewVideoActivity.EXTRA_AUTOPLAY, true);
                 secondFragment = PreviewMediaFragment.newInstance(file, getAccount(), startPlaybackPosition, autoplay);
-            } else if (file.isDown() && PreviewTextFragment.canBePreviewed(file)) {
+            } else if (file.isDown() && PreviewTextFileFragment.canBePreviewed(file)) {
                 secondFragment = null;
             } else {
                 secondFragment = FileDetailFragment.newInstance(file, getAccount());
@@ -739,7 +741,7 @@ public class FileDisplayActivity extends FileActivity
                         } else if (MimeTypeUtil.isVCard(mWaitingToPreview.getMimeType())) {
                             startContactListFragment(mWaitingToPreview);
                             detailsFragmentChanged = true;
-                        } else if (PreviewTextFragment.canBePreviewed(mWaitingToPreview)) {
+                        } else if (PreviewTextFileFragment.canBePreviewed(mWaitingToPreview)) {
                             startTextPreview(mWaitingToPreview, true);
                             detailsFragmentChanged = true;
                         } else {
@@ -1504,7 +1506,7 @@ public class FileDisplayActivity extends FileActivity
                         OCFile ocFile = getFile();
                         if (PreviewImageFragment.canBePreviewed(ocFile)) {
                             startImagePreview(getFile(), true);
-                        } else if (PreviewTextFragment.canBePreviewed(ocFile)) {
+                        } else if (PreviewTextFileFragment.canBePreviewed(ocFile)) {
                             startTextPreview(ocFile, true);
                         }
                         // TODO what about other kind of previews?
@@ -1778,7 +1780,7 @@ public class FileDisplayActivity extends FileActivity
                     ((PreviewMediaFragment) details).updateFile(file);
                 } else if (details instanceof PreviewTextFragment) {
                     // Refresh  OCFile of the fragment
-                    ((PreviewTextFragment) details).updateFile(file);
+                    ((PreviewTextFileFragment) details).updateFile(file);
                 } else {
                     showDetails(file);
                 }
@@ -2060,8 +2062,8 @@ public class FileDisplayActivity extends FileActivity
                     }
                 } else if (details instanceof PreviewTextFragment &&
                         renamedFile.equals(details.getFile())) {
-                    ((PreviewTextFragment) details).updateFile(renamedFile);
-                    if (PreviewTextFragment.canBePreviewed(renamedFile)) {
+                    ((PreviewTextFileFragment) details).updateFile(renamedFile);
+                    if (PreviewTextFileFragment.canBePreviewed(renamedFile)) {
                         startTextPreview(renamedFile, true);
                     } else {
                         getFileOperationsHelper().openFile(renamedFile);
@@ -2385,6 +2387,23 @@ public class FileDisplayActivity extends FileActivity
         }
     }
 
+    /**
+     * Stars rich workspace preview for a folder.
+     *
+     * @param folder {@link OCFile} to preview its rich workspace.
+     */
+    public void startRichWorkspacePreview(OCFile folder) {
+        Bundle args = new Bundle();
+        args.putParcelable(EXTRA_FILE, folder);
+        Fragment textPreviewFragment = Fragment.instantiate(getApplicationContext(),
+                                                            PreviewTextStringFragment.class.getName(),
+                                                            args);
+        setSecondFragment(textPreviewFragment);
+        updateFragmentsVisibility(true);
+        updateActionBarTitleAndHomeButton(folder);
+        setFile(folder);
+    }
+
     public void startContactListFragment(OCFile file) {
         Intent intent = new Intent(this, ContactsPreferenceActivity.class);
         intent.putExtra(ContactListFragment.FILE_NAME, Parcels.wrap(file));

+ 40 - 165
src/main/java/com/owncloud/android/ui/activity/RichDocumentsWebView.java → src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.java

@@ -24,47 +24,36 @@
 
 package com.owncloud.android.ui.activity;
 
-import android.accounts.Account;
 import android.annotation.SuppressLint;
 import android.app.DownloadManager;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
-import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
-import android.text.TextUtils;
 import android.view.KeyEvent;
-import android.view.View;
 import android.webkit.JavascriptInterface;
 import android.webkit.ValueCallback;
 import android.webkit.WebChromeClient;
 import android.webkit.WebView;
-import android.widget.ImageView;
-import android.widget.ProgressBar;
-import android.widget.TextView;
 import android.widget.Toast;
 
 import com.bumptech.glide.Glide;
-import com.google.android.material.snackbar.Snackbar;
 import com.nextcloud.client.account.CurrentAccountProvider;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.network.ClientFactory;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.Template;
-import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.operations.RichDocumentsCreateAssetOperation;
-import com.owncloud.android.ui.asynctasks.LoadUrlTask;
 import com.owncloud.android.ui.asynctasks.PrintAsyncTask;
 import com.owncloud.android.ui.fragment.OCFileListFragment;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.FileStorageUtils;
-import com.owncloud.android.utils.MimeTypeUtil;
 import com.owncloud.android.utils.glide.CustomGlideStreamLoader;
 
 import org.json.JSONException;
@@ -77,42 +66,25 @@ import java.lang.ref.WeakReference;
 import javax.inject.Inject;
 
 import androidx.annotation.RequiresApi;
-import butterknife.BindView;
 import butterknife.ButterKnife;
 import butterknife.Unbinder;
-import lombok.Getter;
-import lombok.Setter;
 
 /**
  * Opens document for editing via Richdocuments app in a web view
  */
 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class RichDocumentsWebView extends ExternalSiteWebView {
-
-    public static final int MINIMUM_API = Build.VERSION_CODES.LOLLIPOP;
+public class RichDocumentsEditorWebView extends EditorWebView {
     public static final int REQUEST_LOCAL_FILE = 101;
     private static final int REQUEST_REMOTE_FILE = 100;
-    private static final String TAG = RichDocumentsWebView.class.getSimpleName();
     private static final String URL = "URL";
     private static final String TYPE = "Type";
     private static final String PRINT = "print";
     private static final String NEW_NAME = "NewName";
 
     private Unbinder unbinder;
-    private OCFile file;
-    @Getter @Setter private Snackbar loadingSnackbar;
 
     public ValueCallback<Uri[]> uploadMessage;
 
-    @BindView(R.id.progressBar2)
-    ProgressBar progressBar;
-
-    @BindView(R.id.thumbnail)
-    ImageView thumbnail;
-
-    @BindView(R.id.filename)
-    TextView fileName;
-
     @Inject
     protected CurrentAccountProvider currentAccountProvider;
 
@@ -122,54 +94,15 @@ public class RichDocumentsWebView extends ExternalSiteWebView {
     @SuppressLint("AddJavascriptInterface") // suppress warning as webview is only used >= Lollipop
     @Override
     protected void onCreate(Bundle savedInstanceState) {
-        showToolbar = false;
-        webViewLayout = R.layout.richdocuments_webview;
         super.onCreate(savedInstanceState);
 
-        unbinder = ButterKnife.bind(this);
-
-        file = getIntent().getParcelableExtra(EXTRA_FILE);
-
-        // TODO make file nullable
-        if (file == null) {
-            fileName.setText(R.string.create_file_from_template);
-
-            Template template = Parcels.unwrap(getIntent().getParcelableExtra(EXTRA_TEMPLATE));
-
-            int placeholder;
-
-            switch (template.getType()) {
-                case "document":
-                    placeholder = R.drawable.file_doc;
-                    break;
-
-                case "spreadsheet":
-                    placeholder = R.drawable.file_xls;
-                    break;
-
-                case "presentation":
-                    placeholder = R.drawable.file_ppt;
-                    break;
 
-                default:
-                    placeholder = R.drawable.file;
-                    break;
-            }
-
-            Glide.with(this).using(new CustomGlideStreamLoader(currentAccountProvider, clientFactory))
-                .load(template.getThumbnailLink())
-                .placeholder(placeholder)
-                .error(placeholder)
-                .into(thumbnail);
-        } else {
-            setThumbnail(file, thumbnail);
-            fileName.setText(file.getFileName());
-        }
+        unbinder = ButterKnife.bind(this);
 
         webview.addJavascriptInterface(new RichDocumentsMobileInterface(), "RichDocumentsMobileInterface");
 
         webview.setWebChromeClient(new WebChromeClient() {
-            RichDocumentsWebView activity = RichDocumentsWebView.this;
+            RichDocumentsEditorWebView activity = RichDocumentsEditorWebView.this;
 
             @Override
             public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
@@ -197,70 +130,49 @@ public class RichDocumentsWebView extends ExternalSiteWebView {
         });
 
         // load url in background
-        url = getIntent().getStringExtra(EXTRA_URL);
-        if (TextUtils.isEmpty(url)) {
-            new LoadUrlTask(this, getAccount()).execute(file.getLocalId());
-        } else {
-            webview.loadUrl(url);
-        }
+        loadUrl(getIntent().getStringExtra(EXTRA_URL), file);
     }
 
-    private void setThumbnail(OCFile file, ImageView thumbnailView) {
-        // Todo minimize: only icon by mimetype
+    @Override
+    protected void initLoadingScreen() {
+        if (file == null) {
+            fileName.setText(R.string.create_file_from_template);
 
-        if (file.isFolder()) {
-            thumbnailView.setImageDrawable(MimeTypeUtil.getFolderTypeIcon(file.isSharedWithMe() ||
-                                                                              file.isSharedWithSharee(), file.isSharedViaLink(), file.isEncrypted(), file.getMountType(),
-                                                                          this));
-        } else {
-            if ((MimeTypeUtil.isImage(file) || MimeTypeUtil.isVideo(file)) && file.getRemoteId() != null) {
-                // Thumbnail in cache?
-                Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
-                    ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId());
-
-                if (thumbnail != null && !file.isUpdateThumbnailNeeded()) {
-                    if (MimeTypeUtil.isVideo(file)) {
-                        Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail);
-                        thumbnailView.setImageBitmap(withOverlay);
-                    } else {
-                        thumbnailView.setImageBitmap(thumbnail);
-                    }
-                } else {
-                    // generate new thumbnail
-                    if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView)) {
-                        try {
-                            final ThumbnailsCacheManager.ThumbnailGenerationTask task =
-                                new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView,
-                                                                                   getStorageManager(), getAccount());
-
-                            if (thumbnail == null) {
-                                if (MimeTypeUtil.isVideo(file)) {
-                                    thumbnail = ThumbnailsCacheManager.mDefaultVideo;
-                                } else {
-                                    thumbnail = ThumbnailsCacheManager.mDefaultImg;
-                                }
-                            }
-                            final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable =
-                                new ThumbnailsCacheManager.AsyncThumbnailDrawable(getResources(), thumbnail, task);
-                            thumbnailView.setImageDrawable(asyncDrawable);
-                            task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file,
-                                                                                                  file.getRemoteId()));
-                        } catch (IllegalArgumentException e) {
-                            Log_OC.d(TAG, "ThumbnailGenerationTask : " + e.getMessage());
-                        }
-                    }
-                }
+            Template template = Parcels.unwrap(getIntent().getParcelableExtra(EXTRA_TEMPLATE));
 
-                if ("image/png".equalsIgnoreCase(file.getMimeType())) {
-                    thumbnailView.setBackgroundColor(getResources().getColor(R.color.bg_default));
-                }
-            } else {
-                thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimeType(), file.getFileName(),
-                                                                            getAccount(), this));
+            int placeholder;
+
+            switch (template.getType()) {
+                case DOCUMENT:
+                    placeholder = R.drawable.file_doc;
+                    break;
+
+                case SPREADSHEET:
+                    placeholder = R.drawable.file_xls;
+                    break;
+
+                case PRESENTATION:
+                    placeholder = R.drawable.file_ppt;
+                    break;
+
+                default:
+                    placeholder = R.drawable.file;
+                    break;
             }
+
+            Glide.with(this).using(new CustomGlideStreamLoader(currentAccountProvider, clientFactory))
+                .load(template.getThumbnailLink())
+                .placeholder(placeholder)
+                .error(placeholder)
+                .into(thumbnail);
+        } else {
+            setThumbnail(file, thumbnail);
+            fileName.setText(file.getFileName());
         }
     }
 
+
+
     @Override
     protected void onNewIntent(Intent intent) {
         super.onNewIntent(intent);
@@ -272,13 +184,6 @@ public class RichDocumentsWebView extends ExternalSiteWebView {
         startActivityForResult(action, REQUEST_REMOTE_FILE);
     }
 
-    private void openShareDialog() {
-        Intent intent = new Intent(this, ShareActivity.class);
-        intent.putExtra(FileActivity.EXTRA_FILE, file);
-        intent.putExtra(FileActivity.EXTRA_ACCOUNT, getAccount());
-        startActivity(intent);
-    }
-
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         if (RESULT_OK != resultCode) {
@@ -351,22 +256,6 @@ public class RichDocumentsWebView extends ExternalSiteWebView {
         super.onDestroy();
     }
 
-    public void closeView() {
-        webview.destroy();
-        finish();
-    }
-
-    private void hideLoading() {
-        thumbnail.setVisibility(View.GONE);
-        fileName.setVisibility(View.GONE);
-        progressBar.setVisibility(View.GONE);
-        webview.setVisibility(View.VISIBLE);
-
-        if (loadingSnackbar != null) {
-            loadingSnackbar.dismiss();
-        }
-    }
-
     @Override
     protected void onResume() {
         super.onResume();
@@ -403,25 +292,15 @@ public class RichDocumentsWebView extends ExternalSiteWebView {
         downloadmanager.enqueue(request);
     }
 
-    private class RichDocumentsMobileInterface {
-        @JavascriptInterface
-        public void close() {
-            runOnUiThread(RichDocumentsWebView.this::closeView);
-        }
-
+    private class RichDocumentsMobileInterface extends MobileInterface {
         @JavascriptInterface
         public void insertGraphic() {
             openFileChooser();
         }
 
-        @JavascriptInterface
-        public void share() {
-            openShareDialog();
-        }
-
         @JavascriptInterface
         public void documentLoaded() {
-            runOnUiThread(RichDocumentsWebView.this::hideLoading);
+            runOnUiThread(RichDocumentsEditorWebView.this::hideLoading);
         }
 
         @JavascriptInterface
@@ -440,8 +319,6 @@ public class RichDocumentsWebView extends ExternalSiteWebView {
                 Log_OC.e(this, "Failed to parse download json message: " + e);
                 return;
             }
-
-
         }
 
         @JavascriptInterface
@@ -466,6 +343,4 @@ public class RichDocumentsWebView extends ExternalSiteWebView {
             }
         }
     }
-
-
 }

+ 67 - 0
src/main/java/com/owncloud/android/ui/activity/TextEditorWebView.kt

@@ -0,0 +1,67 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2019 Tobias Kaminsky
+ * Copyright (C) 2019 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.activity
+
+import android.annotation.SuppressLint
+import android.content.pm.PackageManager.NameNotFoundException
+import android.os.Build
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import com.owncloud.android.R
+import com.owncloud.android.files.FileMenuFilter
+import com.owncloud.android.lib.common.utils.Log_OC
+
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+class TextEditorWebView : EditorWebView() {
+
+    @SuppressLint("AddJavascriptInterface")
+    // suppress warning as webview is only used >= Lollipop
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val editor = FileMenuFilter.getEditor(contentResolver, account, file.mimeType)
+
+        if (editor != null && editor.id == "onlyoffice") {
+            webview.settings.userAgentString = generateOnlyOfficeUserAgent()
+        }
+
+        webview.addJavascriptInterface(MobileInterface(), "DirectEditingMobileInterface")
+
+        loadUrl(intent.getStringExtra(ExternalSiteWebView.EXTRA_URL), file)
+    }
+
+    private fun generateOnlyOfficeUserAgent(): String {
+        val appString = applicationContext.resources.getString(R.string.only_office_user_agent)
+        val packageName = applicationContext.packageName
+        val androidVersion = Build.VERSION.RELEASE
+        var appVersion = ""
+        try {
+            val pInfo = applicationContext.packageManager.getPackageInfo(packageName, 0)
+            if (pInfo != null) {
+                appVersion = pInfo.versionName
+            }
+        } catch (e: NameNotFoundException) {
+            Log_OC.e(this, "Trying to get packageName", e.cause)
+        }
+        return String.format(appString, androidVersion, appVersion)
+    }
+}

+ 66 - 11
src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java

@@ -24,7 +24,6 @@
 
 package com.owncloud.android.ui.adapter;
 
-import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.content.ContentValues;
 import android.content.Context;
@@ -73,6 +72,7 @@ import com.owncloud.android.ui.TextDrawable;
 import com.owncloud.android.ui.activity.ComponentsGetter;
 import com.owncloud.android.ui.fragment.ExtendedListFragment;
 import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface;
+import com.owncloud.android.ui.preview.PreviewTextFragment;
 import com.owncloud.android.utils.BitmapUtils;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.FileSortOrder;
@@ -128,6 +128,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
     private static final int VIEWTYPE_FOOTER = 0;
     private static final int VIEWTYPE_ITEM = 1;
     private static final int VIEWTYPE_IMAGE = 2;
+    private static final int VIEWTYPE_HEADER = 3;
 
     private List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks = new ArrayList<>();
     private boolean onlyOnDevice;
@@ -270,7 +271,11 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
 
     @Override
     public int getItemCount() {
-        return mFiles.size() + 1;
+        if (shouldShowHeader()) {
+            return mFiles.size() + 2; // for header and footer
+        } else {
+            return mFiles.size() + 1; // for footer
+        }
     }
 
     @NonNull
@@ -299,6 +304,15 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             case VIEWTYPE_FOOTER:
                 View itemView = LayoutInflater.from(mContext).inflate(R.layout.list_footer, parent, false);
                 return new OCFileListFooterViewHolder(itemView);
+
+            case VIEWTYPE_HEADER:
+                View headerView = LayoutInflater.from(mContext).inflate(R.layout.list_header, parent, false);
+
+                ViewGroup.LayoutParams layoutParams = headerView.getLayoutParams();
+                layoutParams.height = (int) (parent.getHeight() * 0.3);
+                headerView.setLayoutParams(layoutParams);
+
+                return new OCFileListHeaderViewHolder(headerView);
         }
     }
 
@@ -311,10 +325,16 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                                                                                    PorterDuff.Mode.SRC_IN);
             footerViewHolder.progressBar.setVisibility(
                 ocFileListFragmentInterface.isLoading() ? View.VISIBLE : View.GONE);
+        } else if (holder instanceof OCFileListHeaderViewHolder) {
+            OCFileListHeaderViewHolder headerViewHolder = (OCFileListHeaderViewHolder) holder;
+            String text = currentDirectory.getRichWorkspace();
+
+            PreviewTextFragment.setText(headerViewHolder.headerText, text, mContext);
+            headerViewHolder.headerView.setOnClickListener(v -> ocFileListFragmentInterface.onHeaderClicked());
         } else {
             OCFileListGridImageViewHolder gridViewHolder = (OCFileListGridImageViewHolder) holder;
 
-            OCFile file = mFiles.get(position);
+            OCFile file = getItem(position);
 
             boolean gridImage = MimeTypeUtil.isImage(file) || MimeTypeUtil.isVideo(file);
 
@@ -620,7 +640,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         OCFile file;
         final boolean showHiddenFiles = preferences.isShowHiddenFilesEnabled();
         for (int i = 0; i < count; i++) {
-            file = getItem(i);
+            file = mFiles.get(i);
             if (file.isFolder()) {
                 foldersCount++;
             } else {
@@ -653,20 +673,42 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
     }
 
     public OCFile getItem(int position) {
-        return mFiles.get(position);
+        if (shouldShowHeader()) {
+            return mFiles.get(position - 1);
+        } else {
+            return mFiles.get(position);
+        }
+    }
+
+    private boolean shouldShowHeader() {
+        if (currentDirectory == null) {
+            return false;
+        }
+
+        return !TextUtils.isEmpty(currentDirectory.getRichWorkspace());
     }
 
     @Override
     public int getItemViewType(int position) {
-        if (position == mFiles.size()) {
-            return VIEWTYPE_FOOTER;
-        } else {
-            if (MimeTypeUtil.isImageOrVideo(getItem(position))) {
-                return VIEWTYPE_IMAGE;
+        if (shouldShowHeader()) {
+            if (position == 0) {
+                return VIEWTYPE_HEADER;
             } else {
-                return VIEWTYPE_ITEM;
+                if (position == mFiles.size() + 1) {
+                    return VIEWTYPE_FOOTER;
+                }
+            }
+        } else {
+            if (position == mFiles.size()) {
+                return VIEWTYPE_FOOTER;
             }
         }
+
+        if (MimeTypeUtil.isImageOrVideo(getItem(position))) {
+            return VIEWTYPE_IMAGE;
+        } else {
+            return VIEWTYPE_ITEM;
+        }
     }
 
     private void showShareIcon(OCFileListGridImageViewHolder gridViewHolder, OCFile file) {
@@ -1135,4 +1177,17 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             ButterKnife.bind(this, itemView);
         }
     }
+
+    static class OCFileListHeaderViewHolder extends RecyclerView.ViewHolder {
+        @BindView(R.id.headerView)
+        public View headerView;
+
+        @BindView(R.id.headerText)
+        public TextView headerText;
+
+        private OCFileListHeaderViewHolder(View itemView) {
+            super(itemView);
+            ButterKnife.bind(this, itemView);
+        }
+    }
 }

+ 38 - 42
src/main/java/com/owncloud/android/ui/asynctasks/LoadUrlTask.java

@@ -22,42 +22,56 @@ package com.owncloud.android.ui.asynctasks;
 
 import android.accounts.Account;
 import android.os.AsyncTask;
-import android.os.Build;
-import android.os.Handler;
-import android.view.View;
-import android.widget.Toast;
 
-import com.google.android.material.snackbar.Snackbar;
-import com.owncloud.android.R;
+import com.nextcloud.android.lib.resources.directediting.DirectEditingOpenFileRemoteOperation;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.files.FileMenuFilter;
+import com.owncloud.android.lib.common.Editor;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.operations.RichDocumentsUrlOperation;
-import com.owncloud.android.ui.activity.RichDocumentsWebView;
-import com.owncloud.android.utils.DisplayUtils;
-import com.owncloud.android.utils.ThemeUtils;
+import com.owncloud.android.ui.activity.EditorWebView;
+import com.owncloud.android.ui.activity.RichDocumentsEditorWebView;
+import com.owncloud.android.ui.activity.TextEditorWebView;
 
 import java.lang.ref.WeakReference;
 
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class LoadUrlTask extends AsyncTask<String, Void, String> {
+public class LoadUrlTask extends AsyncTask<Void, Void, String> {
 
     private Account account;
-    private WeakReference<RichDocumentsWebView> richDocumentsWebViewWeakReference;
+    private WeakReference<EditorWebView> editorWebViewWeakReference;
+    private OCFile file;
 
-    public LoadUrlTask(RichDocumentsWebView richDocumentsWebView, Account account) {
+    public LoadUrlTask(EditorWebView editorWebView, Account account, OCFile file) {
         this.account = account;
-        this.richDocumentsWebViewWeakReference = new WeakReference<>(richDocumentsWebView);
+        this.editorWebViewWeakReference = new WeakReference<>(editorWebView);
+        this.file = file;
     }
 
     @Override
-    protected String doInBackground(String... fileId) {
-        if (richDocumentsWebViewWeakReference.get() == null) {
+    protected String doInBackground(Void... voids) {
+        final EditorWebView editorWebView = editorWebViewWeakReference.get();
+
+        if (editorWebView == null) {
+            return "";
+        }
+
+        RemoteOperationResult result;
+
+
+        if (editorWebView instanceof RichDocumentsEditorWebView) {
+            result = new RichDocumentsUrlOperation(file.getLocalId()).execute(account, editorWebView);
+        } else if (editorWebView instanceof TextEditorWebView) {
+            Editor editor = FileMenuFilter.getEditor(editorWebView.getContentResolver(), account, file.getMimeType());
+
+            if (editor == null) {
+                return "";
+            }
+
+            result = new DirectEditingOpenFileRemoteOperation(file.getRemotePath(), editor.id)
+                .execute(account, editorWebViewWeakReference.get());
+        } else {
             return "";
         }
-        RichDocumentsUrlOperation richDocumentsUrlOperation = new RichDocumentsUrlOperation(fileId[0]);
-        RemoteOperationResult result = richDocumentsUrlOperation.execute(account,
-                                                                         richDocumentsWebViewWeakReference.get());
 
         if (!result.isSuccess()) {
             return "";
@@ -68,30 +82,12 @@ public class LoadUrlTask extends AsyncTask<String, Void, String> {
 
     @Override
     protected void onPostExecute(String url) {
-        RichDocumentsWebView richDocumentsWebView = richDocumentsWebViewWeakReference.get();
+        EditorWebView editorWebView = editorWebViewWeakReference.get();
 
-        if (richDocumentsWebView == null) {
+        if (editorWebView == null) {
             return;
         }
 
-        if (!url.isEmpty()) {
-            richDocumentsWebView.getWebview().loadUrl(url);
-
-            new Handler().postDelayed(() -> {
-                if (richDocumentsWebView.getWebview().getVisibility() != View.VISIBLE) {
-                    Snackbar snackbar = DisplayUtils.createSnackbar(richDocumentsWebView.findViewById(android.R.id.content),
-                                                                    R.string.timeout_richDocuments, Snackbar.LENGTH_INDEFINITE)
-                        .setAction(R.string.fallback_weblogin_back, v -> richDocumentsWebView.closeView());
-
-                    ThemeUtils.colorSnackbar(richDocumentsWebView.getApplicationContext(),snackbar);
-                    richDocumentsWebView.setLoadingSnackbar(snackbar);
-                    snackbar.show();
-                }
-            }, 10 * 1000);
-        } else {
-            Toast.makeText(richDocumentsWebView.getApplicationContext(),
-                           R.string.richdocuments_failed_to_load_document, Toast.LENGTH_LONG).show();
-            richDocumentsWebView.finish();
-        }
+        editorWebView.onUrlLoaded(url);
     }
 }

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

@@ -29,7 +29,7 @@ import android.print.PrintManager;
 
 import com.owncloud.android.R;
 import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.ui.activity.RichDocumentsWebView;
+import com.owncloud.android.ui.activity.RichDocumentsEditorWebView;
 import com.owncloud.android.ui.adapter.PrintAdapter;
 import com.owncloud.android.utils.DisplayUtils;
 
@@ -55,9 +55,9 @@ public class PrintAsyncTask extends AsyncTask<Void, Void, Boolean> {
 
     private File file;
     private String url;
-    private WeakReference<RichDocumentsWebView> richDocumentsWebViewWeakReference;
+    private WeakReference<RichDocumentsEditorWebView> richDocumentsWebViewWeakReference;
 
-    public PrintAsyncTask(File file, String url, WeakReference<RichDocumentsWebView> richDocumentsWebViewWeakReference) {
+    public PrintAsyncTask(File file, String url, WeakReference<RichDocumentsEditorWebView> richDocumentsWebViewWeakReference) {
         this.file = file;
         this.url = url;
         this.richDocumentsWebViewWeakReference = richDocumentsWebViewWeakReference;
@@ -129,7 +129,7 @@ public class PrintAsyncTask extends AsyncTask<Void, Void, Boolean> {
 
     @Override
     protected void onPostExecute(Boolean result) {
-        RichDocumentsWebView richDocumentsWebView = richDocumentsWebViewWeakReference.get();
+        RichDocumentsEditorWebView richDocumentsWebView = richDocumentsWebViewWeakReference.get();
         richDocumentsWebView.dismissLoadingDialog();
 
         PrintManager printManager = (PrintManager) richDocumentsWebView.getSystemService(PRINT_SERVICE);

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

@@ -52,7 +52,7 @@ import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.ui.activity.ExternalSiteWebView;
-import com.owncloud.android.ui.activity.RichDocumentsWebView;
+import com.owncloud.android.ui.activity.RichDocumentsEditorWebView;
 import com.owncloud.android.ui.adapter.TemplateAdapter;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.ThemeUtils;
@@ -241,7 +241,7 @@ public class ChooseTemplateDialogFragment extends DialogFragment implements Dial
                 if (url.isEmpty()) {
                     DisplayUtils.showSnackMessage(fragment.listView, "Error creating file from template");
                 } else {
-                    Intent collaboraWebViewIntent = new Intent(MainApp.getAppContext(), RichDocumentsWebView.class);
+                    Intent collaboraWebViewIntent = new Intent(MainApp.getAppContext(), RichDocumentsEditorWebView.class);
                     collaboraWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_TITLE, "Collabora");
                     collaboraWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_URL, url);
                     collaboraWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false);

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

@@ -20,6 +20,7 @@
 
 package com.owncloud.android.ui.fragment;
 
+import android.os.Build;
 import android.os.Bundle;
 import android.view.View;
 import android.view.ViewGroup;
@@ -32,7 +33,6 @@ import com.nextcloud.client.device.DeviceInfo;
 import com.owncloud.android.R;
 import com.owncloud.android.lib.resources.status.OCCapability;
 import com.owncloud.android.ui.activity.FileActivity;
-import com.owncloud.android.ui.activity.RichDocumentsWebView;
 import com.owncloud.android.utils.ThemeUtils;
 
 import butterknife.BindView;
@@ -100,7 +100,7 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog {
 
         OCCapability capability = fileActivity.getCapabilities();
         if (capability.getRichDocuments().isTrue() && capability.getRichDocumentsDirectEditing().isTrue() &&
-            android.os.Build.VERSION.SDK_INT >= RichDocumentsWebView.MINIMUM_API &&
+            android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
             capability.getRichDocumentsTemplatesAvailable().isTrue()) {
             templates.setVisibility(View.VISIBLE);
         }

+ 42 - 19
src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -25,8 +25,6 @@
 package com.owncloud.android.ui.fragment;
 
 import android.accounts.Account;
-import android.accounts.AuthenticatorException;
-import android.accounts.OperationCanceledException;
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
@@ -60,9 +58,7 @@ import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.VirtualFolderType;
 import com.owncloud.android.files.FileMenuFilter;
-import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.OwnCloudClient;
-import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -75,7 +71,6 @@ import com.owncloud.android.ui.activity.FileActivity;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
 import com.owncloud.android.ui.activity.FolderPickerActivity;
 import com.owncloud.android.ui.activity.OnEnforceableRefreshListener;
-import com.owncloud.android.ui.activity.RichDocumentsWebView;
 import com.owncloud.android.ui.activity.ToolbarActivity;
 import com.owncloud.android.ui.activity.UploadFilesActivity;
 import com.owncloud.android.ui.adapter.OCFileListAdapter;
@@ -94,7 +89,7 @@ import com.owncloud.android.ui.events.SearchEvent;
 import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface;
 import com.owncloud.android.ui.preview.PreviewImageFragment;
 import com.owncloud.android.ui.preview.PreviewMediaFragment;
-import com.owncloud.android.ui.preview.PreviewTextFragment;
+import com.owncloud.android.ui.preview.PreviewTextFileFragment;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.EncryptionUtils;
 import com.owncloud.android.utils.FileSortOrder;
@@ -108,7 +103,6 @@ import org.greenrobot.eventbus.ThreadMode;
 import org.parceler.Parcels;
 
 import java.io.File;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -483,7 +477,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
         popup.setOnMenuItemClickListener(item -> {
             Set<OCFile> checkedFiles = new HashSet<>();
             checkedFiles.add(file);
-            return onFileActionChosen(item.getItemId(), checkedFiles);
+            return onFileActionChosen(item, checkedFiles);
         });
         popup.show();
     }
@@ -506,6 +500,11 @@ public class OCFileListFragment extends ExtendedListFragment implements
                 .show(requireActivity().getSupportFragmentManager(), DIALOG_CREATE_DOCUMENT);
     }
 
+    @Override
+    public void onHeaderClicked() {
+        ((FileDisplayActivity) mContainerActivity).startRichWorkspacePreview(getCurrentFile());
+    }
+
     /**
      * Handler for multiple selection mode.
      * <p>
@@ -635,7 +634,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
         @Override
         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
             Set<OCFile> checkedFiles = mAdapter.getCheckedItems();
-            return onFileActionChosen(item.getItemId(), checkedFiles);
+            return onFileActionChosen(item, checkedFiles);
         }
 
         /**
@@ -713,7 +712,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
     }
 
     @Override
-    public void onPrepareOptionsMenu(Menu menu) {
+    public void onPrepareOptionsMenu(@NonNull Menu menu) {
         Menu mMenu = menu;
 
         if (mOriginalMenuItems.size() == 0) {
@@ -935,7 +934,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
                         }
                     } else if (file.isDown() && MimeTypeUtil.isVCard(file)) {
                         ((FileDisplayActivity) mContainerActivity).startContactListFragment(file);
-                    } else if (PreviewTextFragment.canBePreviewed(file)) {
+                    } else if (PreviewTextFileFragment.canBePreviewed(file)) {
                         ((FileDisplayActivity) mContainerActivity).startTextPreview(file, false);
                     } else if (file.isDown()) {
                         if (PreviewMediaFragment.canBePreviewed(file)) {
@@ -945,6 +944,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
                             mContainerActivity.getFileOperationsHelper().openFile(file);
                         }
                     } else {
+                        // file not downloaded, check for streaming, remote editing
                         User account = accountManager.getUser();
                         OCCapability capability = mContainerActivity.getStorageManager()
                             .getCapability(account.getAccountName());
@@ -953,8 +953,13 @@ public class OCFileListFragment extends ExtendedListFragment implements
                                 .isMediaStreamingSupported()) {
                             // stream media preview on >= NC14
                             ((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, true);
+                        } else if (FileMenuFilter.isEditorAvailable(requireContext().getContentResolver(),
+                                                                    account.toPlatformAccount(),
+                                                                    file.getMimeType()) &&
+                            android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                            mContainerActivity.getFileOperationsHelper().openFileWithTextEditor(file, getContext());
                         } else if (capability.getRichDocumentsMimeTypeList().contains(file.getMimeType()) &&
-                            android.os.Build.VERSION.SDK_INT >= RichDocumentsWebView.MINIMUM_API &&
+                            android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
                             capability.getRichDocumentsDirectEditing().isTrue()) {
                             mContainerActivity.getFileOperationsHelper().openFileAsRichDocument(file, getContext());
                         } else {
@@ -993,11 +998,11 @@ public class OCFileListFragment extends ExtendedListFragment implements
     /**
      * Start the appropriate action(s) on the currently selected files given menu selected by the user.
      *
-     * @param menuId       Identifier of the action menu selected by the user
+     * @param item       MenuItem selected by the user
      * @param checkedFiles List of files selected by the user on which the action should be performed
      * @return 'true' if the menu selection started any action, 'false' otherwise.
      */
-    public boolean onFileActionChosen(int menuId, Set<OCFile> checkedFiles) {
+    public boolean onFileActionChosen(MenuItem item, Set<OCFile> checkedFiles) {
         if (checkedFiles.isEmpty()) {
             return false;
         }
@@ -1005,7 +1010,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
         if (checkedFiles.size() == SINGLE_SELECTION) {
             /// action only possible on a single file
             OCFile singleFile = checkedFiles.iterator().next();
-            switch (menuId) {
+            switch (item.getItemId()) {
                 case R.id.action_send_share_file: {
                     mContainerActivity.getFileOperationsHelper().sendShareFile(singleFile);
                     return true;
@@ -1018,9 +1023,27 @@ public class OCFileListFragment extends ExtendedListFragment implements
                     mContainerActivity.getFileOperationsHelper().streamMediaFile(singleFile);
                     return true;
                 }
-                case R.id.action_open_file_as_richdocument: {
-                    mContainerActivity.getFileOperationsHelper().openFileAsRichDocument(singleFile, getContext());
-                    return true;
+                case R.id.action_edit: {
+                    Account account = ((FileActivity) mContainerActivity).getUserAccountManager()
+                        .getUser().toPlatformAccount();
+
+                    // should not be necessary, as menu item is filtered, but better play safe
+                    if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                        if (FileMenuFilter.isEditorAvailable(requireContext().getContentResolver(),
+                                                             account,
+                                                             singleFile.getMimeType())) {
+                            mContainerActivity.getFileOperationsHelper().openFileWithTextEditor(singleFile,
+                                                                                                getContext());
+                        } else {
+                            mContainerActivity.getFileOperationsHelper().openFileAsRichDocument(singleFile,
+                                                                                                getContext());
+                        }
+
+                        return true;
+                    } else {
+                        DisplayUtils.showSnackMessage(getView(), "Not supported on older than Android 5");
+                        return false;
+                    }
                 }
                 case R.id.action_rename_file: {
                     RenameFileDialogFragment dialog = RenameFileDialogFragment.newInstance(singleFile);
@@ -1050,7 +1073,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
         }
 
         /// actions possible on a batch of files
-        switch (menuId) {
+        switch (item.getItemId()) {
             case R.id.action_remove_file: {
                 RemoveFilesDialogFragment dialog = RemoveFilesDialogFragment.newInstance(new ArrayList<>(checkedFiles), mActiveActionMode);
                 dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);

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

@@ -67,8 +67,9 @@ import com.owncloud.android.services.OperationsService;
 import com.owncloud.android.ui.activity.ConflictsResolveActivity;
 import com.owncloud.android.ui.activity.ExternalSiteWebView;
 import com.owncloud.android.ui.activity.FileActivity;
-import com.owncloud.android.ui.activity.RichDocumentsWebView;
+import com.owncloud.android.ui.activity.RichDocumentsEditorWebView;
 import com.owncloud.android.ui.activity.ShareActivity;
+import com.owncloud.android.ui.activity.TextEditorWebView;
 import com.owncloud.android.ui.dialog.SendShareDialog;
 import com.owncloud.android.ui.events.EncryptionEvent;
 import com.owncloud.android.ui.events.FavoriteEvent;
@@ -98,6 +99,7 @@ import java.util.regex.Pattern;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.core.content.FileProvider;
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentTransaction;
@@ -278,7 +280,7 @@ public class FileOperationsHelper {
                 Account account = fileActivity.getAccount();
                 OCCapability capability = fileActivity.getStorageManager().getCapability(account.name);
                 if (capability.getRichDocumentsMimeTypeList().contains(file.getMimeType()) &&
-                    android.os.Build.VERSION.SDK_INT >= RichDocumentsWebView.MINIMUM_API &&
+                    android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
                     capability.getRichDocumentsDirectEditing().isTrue()) {
                     openFileAsRichDocument(file, fileActivity);
                     return;
@@ -341,14 +343,33 @@ public class FileOperationsHelper {
         }
     }
 
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
     public void openFileAsRichDocument(OCFile file, Context context) {
-        Intent collaboraWebViewIntent = new Intent(context, RichDocumentsWebView.class);
+        Intent collaboraWebViewIntent = new Intent(context, RichDocumentsEditorWebView.class);
         collaboraWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_TITLE, "Collabora");
         collaboraWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_FILE, file);
         collaboraWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false);
         context.startActivity(collaboraWebViewIntent);
     }
 
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+    public void openFileWithTextEditor(OCFile file, Context context) {
+        Intent textEditorIntent = new Intent(context, TextEditorWebView.class);
+        textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_TITLE, "Text");
+        textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_FILE, file);
+        textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false);
+        context.startActivity(textEditorIntent);
+    }
+
+    public void openRichWorkspaceWithTextEditor(OCFile file, String url, Context context) {
+        Intent textEditorIntent = new Intent(context, TextEditorWebView.class);
+        textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_TITLE, "Text");
+        textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_URL, url);
+        textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_FILE, file);
+        textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false);
+        context.startActivity(textEditorIntent);
+    }
+
     @NonNull
     private Intent createOpenFileIntent(OCFile file) {
         String storagePath = file.getStoragePath();

+ 2 - 0
src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java

@@ -46,4 +46,6 @@ public interface OCFileListFragmentInterface {
     boolean onLongItemClicked(OCFile file);
 
     boolean isLoading();
+
+    void onHeaderClicked();
 }

+ 390 - 0
src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java

@@ -0,0 +1,390 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2019 Tobias Kaminsky
+ * Copyright (C) 2019 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.preview;
+
+import android.accounts.Account;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+
+import com.nextcloud.client.account.UserAccountManager;
+import com.owncloud.android.R;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.files.FileMenuFilter;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.ui.activity.FileDisplayActivity;
+import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
+import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment;
+import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.MimeTypeUtil;
+
+import org.jetbrains.annotations.NotNull;
+import org.mozilla.universalchardet.ReaderFactory;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.lang.ref.WeakReference;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Scanner;
+
+import javax.inject.Inject;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.widget.SearchView;
+import androidx.core.view.MenuItemCompat;
+
+public class PreviewTextFileFragment extends PreviewTextFragment {
+    private static final String EXTRA_FILE = "FILE";
+    private static final String EXTRA_ACCOUNT = "ACCOUNT";
+    private static final String TAG = PreviewTextFileFragment.class.getSimpleName();
+
+    private TextLoadAsyncTask textLoadAsyncTask;
+    private Account account;
+
+    @Inject UserAccountManager accountManager;
+
+    /**
+     * Creates an empty fragment for previews.
+     * <p>
+     * MUST BE KEPT: the system uses it when tries to re-instantiate a fragment automatically (for instance, when the
+     * device is turned a aside).
+     * <p>
+     * DO NOT CALL IT: an {@link OCFile} and {@link Account} must be provided for a successful construction
+     */
+    public PreviewTextFileFragment() {
+        super();
+        account = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setHasOptionsMenu(true);
+
+        OCFile file = getFile();
+
+        Bundle args = getArguments();
+
+        if (file == null) {
+            file = args.getParcelable(FileDisplayActivity.EXTRA_FILE);
+        }
+
+        if (account == null) {
+            account = args.getParcelable(FileDisplayActivity.EXTRA_ACCOUNT);
+        }
+
+        if (args.containsKey(FileDisplayActivity.EXTRA_SEARCH_QUERY)) {
+            mSearchQuery = args.getString(FileDisplayActivity.EXTRA_SEARCH_QUERY);
+        }
+        mSearchOpen = args.getBoolean(FileDisplayActivity.EXTRA_SEARCH, false);
+
+        if (savedInstanceState == null) {
+            if (file == null) {
+                throw new IllegalStateException("Instanced with a NULL OCFile");
+            }
+            if (account == null) {
+                throw new IllegalStateException("Instanced with a NULL ownCloud Account");
+            }
+        } else {
+            file = savedInstanceState.getParcelable(EXTRA_FILE);
+            account = savedInstanceState.getParcelable(EXTRA_ACCOUNT);
+        }
+
+        mHandler = new Handler();
+        setFile(file);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        outState.putParcelable(PreviewTextFileFragment.EXTRA_FILE, getFile());
+        outState.putParcelable(PreviewTextFileFragment.EXTRA_ACCOUNT, account);
+
+        super.onSaveInstanceState(outState);
+    }
+
+    @Override
+    void loadAndShowTextPreview() {
+        textLoadAsyncTask = new TextLoadAsyncTask(new WeakReference<>(mTextPreview));
+        textLoadAsyncTask.execute(getFile().getStoragePath());
+    }
+
+    /**
+     * Reads the file to preview and shows its contents. Too critical to be anonymous.
+     */
+    private class TextLoadAsyncTask extends AsyncTask<Object, Void, StringWriter> {
+        private static final int PARAMS_LENGTH = 1;
+        private final WeakReference<TextView> mTextViewReference;
+
+        private TextLoadAsyncTask(WeakReference<TextView> textView) {
+            mTextViewReference = textView;
+        }
+
+        @Override
+        protected void onPreExecute() {
+            // not used at the moment
+        }
+
+        @Override
+        protected StringWriter doInBackground(Object... params) {
+            if (params.length != PARAMS_LENGTH) {
+                throw new IllegalArgumentException("The parameter to " + TextLoadAsyncTask.class.getName()
+                                                       + " must be (1) the file location");
+            }
+            String location = (String) params[0];
+
+            Scanner scanner = null;
+            StringWriter source = new StringWriter();
+            BufferedWriter bufferedWriter = new BufferedWriter(source);
+            Reader reader = null;
+
+            try {
+                File file = new File(location);
+                reader = ReaderFactory.createReaderFromFile(file);
+                scanner = new Scanner(reader);
+
+                while (scanner.hasNextLine()) {
+                    bufferedWriter.append(scanner.nextLine());
+                    if (scanner.hasNextLine()) {
+                        bufferedWriter.append("\n");
+                    }
+                }
+                bufferedWriter.close();
+                IOException exc = scanner.ioException();
+                if (exc != null) {
+                    throw exc;
+                }
+            } catch (IOException e) {
+                Log_OC.e(TAG, e.getMessage(), e);
+                finish();
+            } finally {
+                if (reader != null) {
+                    try {
+                        reader.close();
+                    } catch (IOException e) {
+                        Log_OC.e(TAG, e.getMessage(), e);
+                        finish();
+                    }
+                }
+                if (scanner != null) {
+                    scanner.close();
+                }
+            }
+            return source;
+        }
+
+        @Override
+        protected void onPostExecute(final StringWriter stringWriter) {
+            final TextView textView = mTextViewReference.get();
+
+            if (textView != null) {
+                mOriginalText = stringWriter.toString();
+                mSearchView.setOnQueryTextListener(PreviewTextFileFragment.this);
+
+                setText(textView, mOriginalText, getContext());
+
+                if (mSearchOpen) {
+                    mSearchView.setQuery(mSearchQuery, true);
+                }
+                textView.setVisibility(View.VISIBLE);
+            }
+
+            if (mMultiView != null) {
+                mMultiView.setVisibility(View.GONE);
+            }
+
+        }
+
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onCreateOptionsMenu(@NotNull Menu menu, @NotNull MenuInflater inflater) {
+        super.onCreateOptionsMenu(menu, inflater);
+        inflater.inflate(R.menu.item_file, menu);
+
+        MenuItem menuItem = menu.findItem(R.id.action_search);
+        menuItem.setVisible(true);
+        mSearchView = (SearchView) MenuItemCompat.getActionView(menuItem);
+        mSearchView.setMaxWidth(Integer.MAX_VALUE);
+
+        if (mSearchOpen) {
+            mSearchView.setIconified(false);
+            mSearchView.setQuery(mSearchQuery, false);
+            mSearchView.clearFocus();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onPrepareOptionsMenu(@NotNull Menu menu) {
+        super.onPrepareOptionsMenu(menu);
+
+        if (containerActivity.getStorageManager() != null) {
+            Account currentAccount = containerActivity.getStorageManager().getAccount();
+            FileMenuFilter mf = new FileMenuFilter(
+                getFile(),
+                currentAccount,
+                containerActivity,
+                getActivity(),
+                false
+            );
+            mf.filter(menu,
+                      true,
+                      accountManager.isMediaStreamingSupported(currentAccount));
+        }
+
+        // additional restriction for this fragment
+        FileMenuFilter.hideMenuItems(
+            menu.findItem(R.id.action_rename_file),
+            menu.findItem(R.id.action_select_all),
+            menu.findItem(R.id.action_move),
+            menu.findItem(R.id.action_download_file),
+            menu.findItem(R.id.action_sync_file),
+            menu.findItem(R.id.action_sync_account),
+            menu.findItem(R.id.action_favorite),
+            menu.findItem(R.id.action_unset_favorite)
+        );
+
+        Boolean dualPane = getResources().getBoolean(R.bool.large_land_layout);
+
+        if (!dualPane) {
+            FileMenuFilter.hideMenuItems(menu.findItem(R.id.action_switch_view),
+                                         menu.findItem(R.id.action_sort)
+            );
+        }
+
+        if (getFile().isSharedWithMe() && !getFile().canReshare()) {
+            FileMenuFilter.hideMenuItem(menu.findItem(R.id.action_send_share_file));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.action_send_share_file: {
+                if (getFile().isSharedWithMe() && !getFile().canReshare()) {
+                    DisplayUtils.showSnackMessage(getView(), R.string.resharing_is_not_allowed);
+                } else {
+                    containerActivity.getFileOperationsHelper().sendShareFile(getFile());
+                }
+                return true;
+            }
+            case R.id.action_open_file_with: {
+                openFile();
+                return true;
+            }
+            case R.id.action_remove_file: {
+                RemoveFilesDialogFragment dialog = RemoveFilesDialogFragment.newInstance(getFile());
+                dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
+                return true;
+            }
+            case R.id.action_see_details: {
+                seeDetails();
+                return true;
+            }
+            case R.id.action_sync_file: {
+                containerActivity.getFileOperationsHelper().syncFile(getFile());
+                return true;
+            }
+
+            default:
+                return super.onOptionsItemSelected(item);
+        }
+    }
+
+    /**
+     * Update the file of the fragment with file value
+     *
+     * @param file The new file to set
+     */
+    public void updateFile(OCFile file) {
+        setFile(file);
+    }
+
+    private void seeDetails() {
+        containerActivity.showDetails(getFile());
+    }
+
+    /**
+     * Opens the previewed file with an external application.
+     */
+    private void openFile() {
+        containerActivity.getFileOperationsHelper().openFile(getFile());
+        finish();
+    }
+
+    /**
+     * Helper method to test if an {@link OCFile} can be passed to a {@link PreviewTextFileFragment} to be previewed.
+     *
+     * @param file File to test if can be previewed.
+     * @return 'True' if the file can be handled by the fragment.
+     */
+    public static boolean canBePreviewed(OCFile file) {
+        final List<String> unsupportedTypes = new LinkedList<>();
+        unsupportedTypes.add("text/richtext");
+        unsupportedTypes.add("text/rtf");
+        unsupportedTypes.add("text/calendar");
+        unsupportedTypes.add("text/vnd.abc");
+        unsupportedTypes.add("text/vnd.fmi.flexstor");
+        unsupportedTypes.add("text/vnd.rn-realtext");
+        unsupportedTypes.add("text/vnd.wap.wml");
+        unsupportedTypes.add("text/vnd.wap.wmlscript");
+        return file != null && file.isDown() && MimeTypeUtil.isText(file) &&
+            !unsupportedTypes.contains(file.getMimeType()) &&
+            !unsupportedTypes.contains(MimeTypeUtil.getMimeTypeFromPath(file.getRemotePath()));
+    }
+
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        Log_OC.e(TAG, "onStop");
+
+        if (textLoadAsyncTask != null) {
+            textLoadAsyncTask.cancel(Boolean.TRUE);
+        }
+    }
+
+}

+ 16 - 365
src/main/java/com/owncloud/android/ui/preview/PreviewTextFragment.java

@@ -19,21 +19,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;
-import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
@@ -44,35 +39,16 @@ import android.widget.TextView;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.di.Injectable;
 import com.owncloud.android.R;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.files.FileMenuFilter;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
-import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
-import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment;
 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;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.IOException;
-import java.io.Reader;
-import java.io.StringWriter;
-import java.lang.ref.WeakReference;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Scanner;
-
 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;
@@ -94,46 +70,25 @@ import io.noties.prism4j.annotations.PrismBundle;
     },
     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";
+public abstract class PreviewTextFragment extends FileFragment implements SearchView.OnQueryTextListener, Injectable {
     private static final String TAG = PreviewTextFragment.class.getSimpleName();
 
-    private Account mAccount;
-    private TextView mTextPreview;
-    private TextLoadAsyncTask mTextLoadTask;
 
-    private String mOriginalText;
-
-    private Handler mHandler;
-    private SearchView mSearchView;
-    private RelativeLayout mMultiView;
+    protected SearchView mSearchView;
+    protected String mSearchQuery = "";
+    protected boolean mSearchOpen;
+    protected TextView mTextPreview;
+    protected Handler mHandler;
+    protected RelativeLayout mMultiView;
+    protected String mOriginalText;
 
     private TextView mMultiListMessage;
     private TextView mMultiListHeadline;
     private ImageView mMultiListIcon;
     private ProgressBar mMultiListProgress;
 
-
-    private String mSearchQuery = "";
-    private boolean mSearchOpen;
-
     @Inject UserAccountManager accountManager;
 
-    /**
-     * Creates an empty fragment for previews.
-     *
-     * MUST BE KEPT: the system uses it when tries to re-instantiate a fragment automatically
-     * (for instance, when the device is turned a aside).
-     *
-     * DO NOT CALL IT: an {@link OCFile} and {@link Account} must be provided for a successful
-     * construction
-     */
-    public PreviewTextFragment() {
-        super();
-        mAccount = null;
-    }
-
     /**
      * {@inheritDoc}
      */
@@ -171,59 +126,6 @@ public class PreviewTextFragment extends FileFragment implements SearchView.OnQu
         }
     }
 
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setHasOptionsMenu(true);
-
-        OCFile file = getFile();
-
-        Bundle args = getArguments();
-
-        if (file == null) {
-            file = args.getParcelable(FileDisplayActivity.EXTRA_FILE);
-        }
-
-        if (mAccount == null) {
-            mAccount = args.getParcelable(FileDisplayActivity.EXTRA_ACCOUNT);
-        }
-
-        if (args.containsKey(FileDisplayActivity.EXTRA_SEARCH_QUERY)) {
-            mSearchQuery = args.getString(FileDisplayActivity.EXTRA_SEARCH_QUERY);
-        }
-        mSearchOpen = args.getBoolean(FileDisplayActivity.EXTRA_SEARCH, false);
-
-        if (savedInstanceState == null) {
-            if (file == null) {
-                throw new IllegalStateException("Instanced with a NULL OCFile");
-            }
-            if (mAccount == null) {
-                throw new IllegalStateException("Instanced with a NULL ownCloud Account");
-            }
-        } else {
-            file = savedInstanceState.getParcelable(EXTRA_FILE);
-            mAccount = savedInstanceState.getParcelable(EXTRA_ACCOUNT);
-        }
-
-        mHandler = new Handler();
-        setFile(file);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void onSaveInstanceState(@NonNull Bundle outState) {
-        outState.putParcelable(PreviewTextFragment.EXTRA_FILE, getFile());
-        outState.putParcelable(PreviewTextFragment.EXTRA_ACCOUNT, mAccount);
-
-        super.onSaveInstanceState(outState);
-    }
-
     @Override
     public void onStart() {
         super.onStart();
@@ -232,11 +134,7 @@ public class PreviewTextFragment extends FileFragment implements SearchView.OnQu
         loadAndShowTextPreview();
     }
 
-    private void loadAndShowTextPreview() {
-        mTextLoadTask = new TextLoadAsyncTask(new WeakReference<>(mTextPreview));
-        mTextLoadTask.execute(getFile().getStoragePath());
-    }
-
+    abstract void loadAndShowTextPreview();
 
     @Override
     public boolean onQueryTextSubmit(String query) {
@@ -267,7 +165,7 @@ public class PreviewTextFragment extends FileFragment implements SearchView.OnQu
                         mTextPreview.setText(Html.fromHtml(coloredText.replace("\n", "<br \\>")));
                     }
                 } else {
-                    setText(mTextPreview, mOriginalText, getFile());
+                    setText(mTextPreview, mOriginalText, getContext());
                 }
             }, delay);
         }
@@ -277,7 +175,7 @@ public class PreviewTextFragment extends FileFragment implements SearchView.OnQu
         }
     }
 
-    private Spanned getRenderedMarkdownText(Context context, String markdown) {
+    protected static 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);
@@ -302,263 +200,16 @@ public class PreviewTextFragment extends FileFragment implements SearchView.OnQu
         return markwon.toMarkdown(markdown);
     }
 
-    /**
-     * Reads the file to preview and shows its contents. Too critical to be anonymous.
-     */
-    private class TextLoadAsyncTask extends AsyncTask<Object, Void, StringWriter> {
-        private static final int PARAMS_LENGTH = 1;
-        private final WeakReference<TextView> mTextViewReference;
-
-        private TextLoadAsyncTask(WeakReference<TextView> textView) {
-            mTextViewReference = textView;
-        }
-
-        @Override
-        protected void onPreExecute() {
-            // not used at the moment
-        }
-
-        @Override
-        protected StringWriter doInBackground(Object... params) {
-            if (params.length != PARAMS_LENGTH) {
-                throw new IllegalArgumentException("The parameter to " + TextLoadAsyncTask.class.getName()
-                        + " must be (1) the file location");
-            }
-            String location = (String) params[0];
-
-            Scanner scanner = null;
-            StringWriter source = new StringWriter();
-            BufferedWriter bufferedWriter = new BufferedWriter(source);
-            Reader reader = null;
-
-            try {
-                File file = new File(location);
-                reader = ReaderFactory.createReaderFromFile(file);
-                scanner = new Scanner(reader);
-
-                while (scanner.hasNextLine()) {
-                    bufferedWriter.append(scanner.nextLine());
-                    if (scanner.hasNextLine()) {
-                        bufferedWriter.append("\n");
-                    }
-                }
-                bufferedWriter.close();
-                IOException exc = scanner.ioException();
-                if (exc != null) {
-                    throw exc;
-                }
-            } catch (IOException e) {
-                Log_OC.e(TAG, e.getMessage(), e);
-                finish();
-            } finally {
-                if (reader != null) {
-                    try {
-                        reader.close();
-                    } catch (IOException e) {
-                        Log_OC.e(TAG, e.getMessage(), e);
-                        finish();
-                    }
-                }
-                if (scanner != null) {
-                    scanner.close();
-                }
-            }
-            return source;
-        }
-
-        @Override
-        protected void onPostExecute(final StringWriter stringWriter) {
-            final TextView textView = mTextViewReference.get();
-
-            if (textView != null) {
-                mOriginalText = stringWriter.toString();
-                mSearchView.setOnQueryTextListener(PreviewTextFragment.this);
-
-                setText(textView, mOriginalText, getFile());
-
-                if (mSearchOpen) {
-                    mSearchView.setQuery(mSearchQuery, true);
-                }
-                textView.setVisibility(View.VISIBLE);
-            }
-
-            if (mMultiView != null) {
-                mMultiView.setVisibility(View.GONE);
-            }
-
-        }
-
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
-        super.onCreateOptionsMenu(menu, inflater);
-        inflater.inflate(R.menu.item_file, menu);
-
-        MenuItem menuItem = menu.findItem(R.id.action_search);
-        menuItem.setVisible(true);
-        mSearchView = (SearchView) MenuItemCompat.getActionView(menuItem);
-        mSearchView.setMaxWidth(Integer.MAX_VALUE);
-
-        if (mSearchOpen) {
-            mSearchView.setIconified(false);
-            mSearchView.setQuery(mSearchQuery, false);
-            mSearchView.clearFocus();
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void onPrepareOptionsMenu(Menu menu) {
-        super.onPrepareOptionsMenu(menu);
-
-        if (containerActivity.getStorageManager() != null) {
-            Account currentAccount = containerActivity.getStorageManager().getAccount();
-            FileMenuFilter mf = new FileMenuFilter(
-                    getFile(),
-                    currentAccount,
-                containerActivity,
-                    getActivity(),
-                    false
-            );
-            mf.filter(menu,
-                      true,
-                      accountManager.isMediaStreamingSupported(currentAccount));
-        }
-
-        // additional restriction for this fragment
-        FileMenuFilter.hideMenuItems(
-                menu.findItem(R.id.action_rename_file),
-                menu.findItem(R.id.action_select_all),
-                menu.findItem(R.id.action_move),
-                menu.findItem(R.id.action_download_file),
-                menu.findItem(R.id.action_sync_file),
-                menu.findItem(R.id.action_sync_account),
-                menu.findItem(R.id.action_favorite),
-                menu.findItem(R.id.action_unset_favorite)
-        );
-
-        Boolean dualPane = getResources().getBoolean(R.bool.large_land_layout);
-
-        if (!dualPane) {
-            FileMenuFilter.hideMenuItems(menu.findItem(R.id.action_switch_view),
-                    menu.findItem(R.id.action_sort)
-            );
-        }
-
-        if(getFile().isSharedWithMe() && !getFile().canReshare()){
-            FileMenuFilter.hideMenuItem(menu.findItem(R.id.action_send_share_file));
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        switch (item.getItemId()) {
-            case R.id.action_send_share_file: {
-                if(getFile().isSharedWithMe() && !getFile().canReshare()){
-                    DisplayUtils.showSnackMessage(getView(), R.string.resharing_is_not_allowed);
-                } else {
-                    containerActivity.getFileOperationsHelper().sendShareFile(getFile());
-                }
-                return true;
-            }
-            case R.id.action_open_file_with: {
-                openFile();
-                return true;
-            }
-            case R.id.action_remove_file: {
-                RemoveFilesDialogFragment dialog = RemoveFilesDialogFragment.newInstance(getFile());
-                dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
-                return true;
-            }
-            case R.id.action_see_details: {
-                seeDetails();
-                return true;
-            }
-            case R.id.action_sync_file: {
-                containerActivity.getFileOperationsHelper().syncFile(getFile());
-                return true;
-            }
-
-            default:
-                return super.onOptionsItemSelected(item);
-        }
-    }
-
-    /**
-     * Update the file of the fragment with file value
-     *
-     * @param file The new file to set
-     */
-    public void updateFile(OCFile file) {
-        setFile(file);
-    }
-
-    private void seeDetails() {
-        containerActivity.showDetails(getFile());
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-        Log_OC.e(TAG, "onStop");
-        if (mTextLoadTask != null) {
-            mTextLoadTask.cancel(Boolean.TRUE);
-        }
-    }
-
-    /**
-     * Opens the previewed file with an external application.
-     */
-    private void openFile() {
-        containerActivity.getFileOperationsHelper().openFile(getFile());
-        finish();
-    }
-
-    /**
-     * Helper method to test if an {@link OCFile} can be passed to a {@link PreviewTextFragment} to be previewed.
-     *
-     * @param file File to test if can be previewed.
-     * @return 'True' if the file can be handled by the fragment.
-     */
-    public static boolean canBePreviewed(OCFile file) {
-        final List<String> unsupportedTypes = new LinkedList<>();
-        unsupportedTypes.add("text/richtext");
-        unsupportedTypes.add("text/rtf");
-        unsupportedTypes.add("text/calendar");
-        unsupportedTypes.add("text/vnd.abc");
-        unsupportedTypes.add("text/vnd.fmi.flexstor");
-        unsupportedTypes.add("text/vnd.rn-realtext");
-        unsupportedTypes.add("text/vnd.wap.wml");
-        unsupportedTypes.add("text/vnd.wap.wmlscript");
-        return file != null && file.isDown() && MimeTypeUtil.isText(file) &&
-                !unsupportedTypes.contains(file.getMimeType()) &&
-                !unsupportedTypes.contains(MimeTypeUtil.getMimeTypeFromPath(file.getRemotePath()));
-    }
-
     /**
      * Finishes the preview
      */
-    private void finish() {
-        getActivity().runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                getActivity().onBackPressed();
-            }
-        });
+    protected void finish() {
+        getActivity().runOnUiThread(() -> getActivity().onBackPressed());
     }
 
-    private void setText(TextView textView, String text, OCFile file) {
-        if (MimeTypeUtil.MIMETYPE_TEXT_MARKDOWN.equals(file.getMimeType())) {
-            textView.setText(getRenderedMarkdownText(getContext(), text));
+    public static void setText(TextView textView, String text, Context context) {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN && context != null) {
+            textView.setText(getRenderedMarkdownText(context, text));
         } else {
             textView.setText(text);
         }

+ 174 - 0
src/main/java/com/owncloud/android/ui/preview/PreviewTextStringFragment.java

@@ -0,0 +1,174 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2019 Tobias Kaminsky
+ * Copyright (C) 2019 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.preview;
+
+import android.accounts.Account;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.nextcloud.android.lib.richWorkspace.RichWorkspaceDirectEditingRemoteOperation;
+import com.nextcloud.client.account.UserAccountManager;
+import com.owncloud.android.R;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.ui.activity.FileDisplayActivity;
+import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.ThemeUtils;
+
+import org.jetbrains.annotations.NotNull;
+
+import javax.inject.Inject;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.widget.SearchView;
+import androidx.core.view.MenuItemCompat;
+
+public class PreviewTextStringFragment extends PreviewTextFragment {
+    private static final String EXTRA_FILE = "FILE";
+
+    @Inject UserAccountManager accountManager;
+
+    /**
+     * Creates an empty fragment for previews.
+     * <p>
+     * MUST BE KEPT: the system uses it when tries to re-instantiate a fragment automatically (for instance, when the
+     * device is turned a aside).
+     * <p>
+     * DO NOT CALL IT: an {@link OCFile} and {@link Account} must be provided for a successful construction
+     */
+    public PreviewTextStringFragment() {
+        super();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setHasOptionsMenu(true);
+
+        Bundle args = getArguments();
+
+        if (args.containsKey(FileDisplayActivity.EXTRA_SEARCH_QUERY)) {
+            mSearchQuery = args.getString(FileDisplayActivity.EXTRA_SEARCH_QUERY);
+        }
+        mSearchOpen = args.getBoolean(FileDisplayActivity.EXTRA_SEARCH, false);
+
+        mHandler = new Handler();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        outState.putParcelable(PreviewTextStringFragment.EXTRA_FILE, getFile());
+
+        super.onSaveInstanceState(outState);
+    }
+
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        View view = super.onCreateView(inflater, container, savedInstanceState);
+
+        if (view == null) {
+            throw new RuntimeException("View may not be null");
+        }
+
+        FloatingActionButton mFabMain = view.findViewById(R.id.text_preview_fab);
+        mFabMain.setVisibility(View.VISIBLE);
+        mFabMain.setEnabled(true);
+        mFabMain.setOnClickListener(v -> edit());
+        ThemeUtils.tintFloatingActionButton(mFabMain, R.drawable.ic_edit, getContext());
+
+        return view;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onCreateOptionsMenu(@NotNull Menu menu, @NotNull MenuInflater inflater) {
+        super.onCreateOptionsMenu(menu, inflater);
+
+        MenuItem menuItem = menu.findItem(R.id.action_search);
+        menuItem.setVisible(true);
+        mSearchView = (SearchView) MenuItemCompat.getActionView(menuItem);
+        mSearchView.setOnQueryTextListener(this);
+        mSearchView.setMaxWidth(Integer.MAX_VALUE);
+
+        if (mSearchOpen) {
+            mSearchView.setIconified(false);
+            mSearchView.setQuery(mSearchQuery, true);
+            mSearchView.clearFocus();
+        }
+    }
+
+    @Override
+    public void onPrepareOptionsMenu(@NonNull Menu menu) {
+        super.onPrepareOptionsMenu(menu);
+
+        menu.findItem(R.id.action_sync_account).setVisible(false);
+        menu.findItem(R.id.action_sort).setVisible(false);
+        menu.findItem(R.id.action_switch_view).setVisible(false);
+    }
+
+    void loadAndShowTextPreview() {
+        if (mTextPreview != null) {
+            mOriginalText = getFile().getRichWorkspace();
+            setText(mTextPreview, mOriginalText, getContext());
+            mTextPreview.setVisibility(View.VISIBLE);
+        }
+
+        if (mMultiView != null) {
+            mMultiView.setVisibility(View.GONE);
+        }
+    }
+
+    private void edit() {
+        new Thread(() -> {
+            RemoteOperationResult result = new RichWorkspaceDirectEditingRemoteOperation(getFile().getRemotePath())
+                .execute(accountManager.getUser().toPlatformAccount(), getContext());
+
+            if (result.isSuccess()) {
+                String url = (String) result.getSingleData();
+                containerActivity.getFileOperationsHelper().openRichWorkspaceWithTextEditor(getFile(),
+                                                                                            url,
+                                                                                            getContext());
+            } else {
+                DisplayUtils.showSnackMessage(getView(), "Error");
+            }
+        }).start();
+    }
+
+    // TODO on close clean search query
+}

+ 1 - 0
src/main/java/com/owncloud/android/utils/FileStorageUtils.java

@@ -223,6 +223,7 @@ public final class FileStorageUtils {
         file.setOwnerDisplayName(remote.getOwnerDisplayName());
         file.setNote(remote.getNote());
         file.setSharees(new ArrayList<>(Arrays.asList(remote.getSharees())));
+        file.setRichWorkspace(remote.getRichWorkspace());
 
         return file;
     }

+ 32 - 0
src/main/res/drawable/ic_edit.xml

@@ -0,0 +1,32 @@
+<!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2019 Tobias Kaminsky
+  ~ Copyright (C) 2019 Nextcloud GmbH
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU Affero General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  ~ GNU Affero General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU Affero General Public License
+  ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+<vector android:autoMirrored="true"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
+</vector>

+ 36 - 0
src/main/res/layout/list_header.xml

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Nextcloud Android client application
+
+  @author Tobias Kaminsky
+  Copyright (C) 2019 Tobias Kaminsky
+  Copyright (C) 2019 Nextcloud GmbH
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU Affero General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU Affero General Public License for more details.
+
+  You should have received a copy of the GNU Affero General Public License
+  along with this program. If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/headerView"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:showDividers="none">
+
+    <TextView
+        android:id="@+id/headerText"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginBottom="@dimen/min_list_item_size"
+        android:padding="@dimen/standard_padding"
+        android:textColor="@color/secondary_text_color" />
+</LinearLayout>

+ 0 - 2
src/main/res/layout/richdocuments_webview.xml

@@ -64,8 +64,6 @@
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:visibility="gone"/>
-
-
     </LinearLayout>
 
     <include

+ 16 - 4
src/main/res/layout/text_file_preview.xml

@@ -21,9 +21,9 @@
             android:layout_height="match_parent"
             android:fillViewport="true">
 
-    <RelativeLayout
+    <androidx.coordinatorlayout.widget.CoordinatorLayout
         android:layout_width="match_parent"
-        android:layout_height="match_parent">
+        android:layout_height="wrap_content">
 
         <TextView
             android:id="@+id/text_preview"
@@ -46,6 +46,18 @@
 
             </ScrollView>
         </RelativeLayout>
-    </RelativeLayout>
 
-</ScrollView>
+        <com.google.android.material.floatingactionbutton.FloatingActionButton
+            android:id="@+id/text_preview_fab"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="end|bottom"
+            android:layout_alignParentEnd="true"
+            android:layout_alignParentRight="true"
+            android:layout_marginBottom="@dimen/standard_margin"
+            android:layout_marginEnd="@dimen/standard_margin"
+            android:layout_marginRight="@dimen/standard_margin"
+            android:contentDescription="@string/fab_label"
+            android:visibility="gone" />
+    </androidx.coordinatorlayout.widget.CoordinatorLayout>
+</ScrollView>

+ 6 - 6
src/main/res/menu/item_file.xml

@@ -22,6 +22,12 @@
       xmlns:tools="http://schemas.android.com/tools"
       tools:ignore="AppCompatResource">
 
+    <item
+        android:id="@+id/action_edit"
+        android:title="@string/action_edit"
+        app:showAsAction="never"
+        android:showAsAction="never" />
+
     <item
         android:id="@+id/action_favorite"
         android:title="@string/favorite"
@@ -84,12 +90,6 @@
         app:showAsAction="never"
         android:showAsAction="never" />
 
-    <item
-        android:id="@+id/action_open_file_as_richdocument"
-        android:title="@string/actionbar_open_as_richdocument_parameter"
-        app:showAsAction="never"
-        android:showAsAction="never"/>
-
     <item
         android:id="@+id/action_sync_file"
         android:title="@string/filedetails_sync_file"

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

@@ -18,6 +18,7 @@
     <string name="data_folder">nextcloud</string>
     <string name="default_display_name_for_root_folder">Nextcloud</string>
     <string name="nextcloud_user_agent">Mozilla/5.0 (Android) Nextcloud-android/%1$s</string>
+    <string name="only_office_user_agent">Mozilla/5.0 (Android %1$s) Mobile Nextcloud-android/%2$s</string>
 
     <!-- URLs and flags related -->
     <string name="server_url"></string>

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

@@ -810,7 +810,6 @@
     <string name="stream_not_possible_headline">Internal streaming not possible</string>
     <string name="stream_not_possible_message">Please download media instead or use external app.</string>
     <string name="folder_already_exists">Folder already exists</string>
-    <string name="actionbar_open_as_richdocument_parameter">Open with %1$s</string>
     <string name="notification_icon">Notification icon</string>
     <string name="folder_confirm_create">Create</string>
     <string name="file_delete">Delete</string>
@@ -908,4 +907,5 @@
     <string name="folder">folder</string>
     <string name="file">file</string>
     <string name="share_internal_link">Share internal link</string>
+    <string name="action_edit">Edit</string>
 </resources>