Browse Source

Direct editing support
- abstract EditorWebView
- support direct editing endpoint

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>

tobiasKaminsky 5 years ago
parent
commit
ec2cfef138
23 changed files with 822 additions and 395 deletions
  1. 1 1
      build.gradle
  2. 1 1
      settings.gradle
  3. 2 0
      src/main/java/com/nextcloud/client/di/ComponentsModule.java
  4. 5 0
      src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  5. 4 0
      src/main/java/com/owncloud/android/datamodel/OCFile.java
  6. 5 1
      src/main/java/com/owncloud/android/db/ProviderMeta.java
  7. 3 0
      src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java
  8. 20 1
      src/main/java/com/owncloud/android/providers/FileContentProvider.java
  9. 1 0
      src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java
  10. 26 7
      src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  11. 0 1
      src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.java
  12. 1 1
      src/main/java/com/owncloud/android/ui/activity/TextEditorWebView.kt
  13. 66 11
      src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java
  14. 8 2
      src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
  15. 9 0
      src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java
  16. 2 0
      src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java
  17. 390 0
      src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java
  18. 16 365
      src/main/java/com/owncloud/android/ui/preview/PreviewTextFragment.java
  19. 177 0
      src/main/java/com/owncloud/android/ui/preview/PreviewTextStringFragment.java
  20. 1 0
      src/main/java/com/owncloud/android/utils/FileStorageUtils.java
  21. 32 0
      src/main/res/drawable/ic_edit.xml
  22. 36 0
      src/main/res/layout/list_header.xml
  23. 16 4
      src/main/res/layout/text_file_preview.xml

+ 1 - 1
build.gradle

@@ -63,7 +63,7 @@ ext {
     daggerVersion = "2.25.3"
     markwonVersion =  "4.2.0"
     prismVersion = "2.0.0"
-    androidLibraryVersion = "master-SNAPSHOT"
+    androidLibraryVersion = "richWorkspace-SNAPSHOT"
 
     travisBuild = System.getenv("TRAVIS") == "true"
 

+ 1 - 1
settings.gradle

@@ -1,2 +1,2 @@
 include ':'
-//include ':nextcloud-android-library'
+//include 'nextcloud-android-library'

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

@@ -77,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;
 
@@ -138,6 +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();

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

@@ -39,6 +39,7 @@ import com.google.gson.Gson;
 import com.google.gson.JsonSyntaxException;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
+import com.owncloud.android.lib.common.DirectEditing;
 import com.owncloud.android.lib.common.network.WebdavEntry;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -215,6 +216,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 +467,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 +508,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;
     }
@@ -1003,6 +1007,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));
 

+ 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 = "";
     }
 
     /**

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

@@ -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";

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

@@ -422,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

+ 20 - 1
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);"
         );
     }
 
@@ -2083,6 +2084,24 @@ 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 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

+ 1 - 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

+ 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));

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

@@ -99,7 +99,6 @@ public class RichDocumentsEditorWebView extends EditorWebView {
 
         unbinder = ButterKnife.bind(this);
 
-        WebView.setWebContentsDebuggingEnabled(true);
         webview.addJavascriptInterface(new RichDocumentsMobileInterface(), "RichDocumentsMobileInterface");
 
         webview.setWebChromeClient(new WebChromeClient() {

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

@@ -33,7 +33,7 @@ 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
+    @SuppressLint("AddJavascriptInterface")
     // suppress warning as webview is only used >= Lollipop
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)

+ 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);
+        }
+    }
 }

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

@@ -89,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;
@@ -100,6 +100,7 @@ import org.apache.commons.httpclient.HttpStatus;
 import org.greenrobot.eventbus.EventBus;
 import org.greenrobot.eventbus.Subscribe;
 import org.greenrobot.eventbus.ThreadMode;
+import org.jetbrains.annotations.NotNull;
 import org.parceler.Parcels;
 
 import java.io.File;
@@ -500,6 +501,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>
@@ -929,7 +935,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)) {

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

@@ -361,6 +361,15 @@ public class FileOperationsHelper {
         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);
         }

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

@@ -0,0 +1,177 @@
+/*
+ *
+ * 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 TAG = PreviewTextStringFragment.class.getSimpleName();
+    private static final String EXTRA_FILE = "FILE";
+
+    private FloatingActionButton mFabMain;
+
+    @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");
+        }
+
+        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>

+ 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>