Эх сурвалжийг харах

Merge pull request #2685 from nextcloud/versions

Add support for file versions
Tobias Kaminsky 7 жил өмнө
parent
commit
9a5a6f5596
21 өөрчлөгдсөн 628 нэмэгдсэн , 75 устгасан
  1. 3 3
      build.gradle
  2. 4 0
      drawable_resources/ic_history.svg
  3. 29 24
      src/main/java/com/owncloud/android/operations/RemoveFileOperation.java
  4. 92 0
      src/main/java/com/owncloud/android/operations/RestoreFileVersionOperation.java
  5. 14 1
      src/main/java/com/owncloud/android/services/OperationsService.java
  6. 66 10
      src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  7. 150 0
      src/main/java/com/owncloud/android/ui/adapter/ActivityAndVersionListAdapter.java
  8. 14 16
      src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java
  9. 7 1
      src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.java
  10. 2 2
      src/main/java/com/owncloud/android/ui/dialog/RemoveFileDialogFragment.java
  11. 2 2
      src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.java
  12. 56 11
      src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java
  13. 9 0
      src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java
  14. 29 2
      src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java
  15. 31 0
      src/main/java/com/owncloud/android/ui/interfaces/VersionListInterface.java
  16. 10 0
      src/main/java/com/owncloud/android/utils/DisplayUtils.java
  17. 11 0
      src/main/res/drawable/ic_history.xml
  18. 89 0
      src/main/res/layout/version_list_item.xml
  19. 3 0
      src/main/res/values/dims.xml
  20. 4 0
      src/main/res/values/strings.xml
  21. 3 3
      src/test/java/com/owncloud/android/utils/ErrorMessageAdapterUnitTest.java

+ 3 - 3
build.gradle

@@ -206,9 +206,9 @@ dependencies {
     // dependencies for app building
     implementation 'com.android.support:multidex:1.0.3'
 //    implementation project('nextcloud-android-library')
-    genericImplementation "com.github.nextcloud:android-library:master-SNAPSHOT"
-    gplayImplementation "com.github.nextcloud:android-library:master-SNAPSHOT"
-    versionDevImplementation 'com.github.nextcloud:android-library:master-SNAPSHOT' // use always latest master
+    genericImplementation "com.github.nextcloud:android-library:fileVersioning-SNAPSHOT"
+    gplayImplementation "com.github.nextcloud:android-library:fileVersioning-SNAPSHOT"
+    versionDevImplementation 'com.github.nextcloud:android-library:fileVersioning-SNAPSHOT' // use always latest master
     implementation "com.android.support:support-v4:${supportLibraryVersion}"
     implementation "com.android.support:design:${supportLibraryVersion}"
     implementation 'com.jakewharton:disklrucache:2.0.2'

+ 4 - 0
drawable_resources/ic_history.svg

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

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

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

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

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

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

@@ -52,6 +52,7 @@ import com.owncloud.android.lib.common.operations.OnRemoteOperationListener;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.lib.resources.files.FileVersion;
 import com.owncloud.android.lib.resources.shares.ShareType;
 import com.owncloud.android.lib.resources.status.OwnCloudVersion;
 import com.owncloud.android.lib.resources.users.GetRemoteUserInfoOperation;
@@ -65,6 +66,7 @@ import com.owncloud.android.operations.MoveFileOperation;
 import com.owncloud.android.operations.OAuth2GetAccessToken;
 import com.owncloud.android.operations.RemoveFileOperation;
 import com.owncloud.android.operations.RenameFileOperation;
+import com.owncloud.android.operations.RestoreFileVersionOperation;
 import com.owncloud.android.operations.SynchronizeFileOperation;
 import com.owncloud.android.operations.SynchronizeFolderOperation;
 import com.owncloud.android.operations.UnshareOperation;
@@ -93,6 +95,7 @@ public class OperationsService extends Service {
     public static final String EXTRA_RESULT = "RESULT";
     public static final String EXTRA_NEW_PARENT_PATH = "NEW_PARENT_PATH";
     public static final String EXTRA_FILE = "FILE";
+    public static final String EXTRA_FILE_VERSION = "FILE_VERSION";
     public static final String EXTRA_SHARE_PASSWORD = "SHARE_PASSWORD";
     public static final String EXTRA_SHARE_TYPE = "SHARE_TYPE";
     public static final String EXTRA_SHARE_WITH = "SHARE_WITH";
@@ -100,6 +103,8 @@ public class OperationsService extends Service {
     public static final String EXTRA_SHARE_PERMISSIONS = "SHARE_PERMISSIONS";
     public static final String EXTRA_SHARE_PUBLIC_UPLOAD = "SHARE_PUBLIC_UPLOAD";
     public static final String EXTRA_SHARE_ID = "SHARE_ID";
+    public static final String EXTRA_USER_ID = "USER_ID";
+    public static final String EXTRA_IN_BACKGROUND = "IN_BACKGROUND";
 
     public static final String EXTRA_COOKIE = "COOKIE";
 
@@ -119,6 +124,7 @@ public class OperationsService extends Service {
     public static final String ACTION_MOVE_FILE = "MOVE_FILE";
     public static final String ACTION_COPY_FILE = "COPY_FILE";
     public static final String ACTION_CHECK_CURRENT_CREDENTIALS = "CHECK_CURRENT_CREDENTIALS";
+    public static final String ACTION_RESTORE_VERSION = "RESTORE_VERSION";
 
     public static final String ACTION_OPERATION_ADDED = OperationsService.class.getName() + ".OPERATION_ADDED";
     public static final String ACTION_OPERATION_FINISHED = OperationsService.class.getName() + ".OPERATION_FINISHED";
@@ -651,7 +657,9 @@ public class OperationsService extends Service {
                     // Remove file or folder
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
                     boolean onlyLocalCopy = operationIntent.getBooleanExtra(EXTRA_REMOVE_ONLY_LOCAL, false);
-                    operation = new RemoveFileOperation(remotePath, onlyLocalCopy, account, getApplicationContext());
+                    boolean inBackground = operationIntent.getBooleanExtra(EXTRA_IN_BACKGROUND, false);
+                    operation = new RemoveFileOperation(remotePath, onlyLocalCopy, account, inBackground,
+                            getApplicationContext());
                     
                 } else if (action.equals(ACTION_CREATE_FOLDER)) {
                     // Create Folder
@@ -691,6 +699,11 @@ public class OperationsService extends Service {
                 } else if (action.equals(ACTION_CHECK_CURRENT_CREDENTIALS)) {
                     // Check validity of currently stored credentials for a given account
                     operation = new CheckCurrentCredentialsOperation(account);
+                } else if (action.equals(ACTION_RESTORE_VERSION)) {
+                    FileVersion fileVersion = operationIntent.getParcelableExtra(EXTRA_FILE_VERSION);
+                    String userId = operationIntent.getStringExtra(EXTRA_USER_ID);
+                    operation = new RestoreFileVersionOperation(fileVersion.getRemoteId(), fileVersion.getFileName(),
+                            userId);
                 }
             }
                 

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

@@ -95,6 +95,7 @@ import com.owncloud.android.operations.MoveFileOperation;
 import com.owncloud.android.operations.RefreshFolderOperation;
 import com.owncloud.android.operations.RemoveFileOperation;
 import com.owncloud.android.operations.RenameFileOperation;
+import com.owncloud.android.operations.RestoreFileVersionOperation;
 import com.owncloud.android.operations.SynchronizeFileOperation;
 import com.owncloud.android.operations.UnshareOperation;
 import com.owncloud.android.operations.UpdateSharePermissionsOperation;
@@ -137,6 +138,7 @@ import org.parceler.Parcels;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
 
 import static com.owncloud.android.db.PreferenceManager.getSortOrder;
 
@@ -588,7 +590,7 @@ public class FileDisplayActivity extends HookActivity
     /**
      * Replaces the second fragment managed by the activity with the received as
      * a parameter.
-     * <p/>
+     *
      * Assumes never will be more than two fragments managed at the same time.
      *
      * @param fragment New second Fragment to set.
@@ -1463,7 +1465,7 @@ public class FileDisplayActivity extends HookActivity
 
     /**
      * Class waiting for broadcast events from the {@link FileDownloader} service.
-     * <p/>
+     *
      * Updates the UI when a download is started or finished, provided that it is relevant for the
      * current folder.
      */
@@ -1726,6 +1728,8 @@ public class FileDisplayActivity extends HookActivity
             onUpdateShareInformation(result, R.string.updating_share_failed);
         } else if (operation instanceof UnshareOperation) {
             onUpdateShareInformation(result, R.string.unsharing_failed);
+        } else if (operation instanceof RestoreFileVersionOperation) {
+            onRestoreFileVersionOperationFinish(result);
         }
     }
 
@@ -1758,15 +1762,21 @@ public class FileDisplayActivity extends HookActivity
      */
     private void onRemoveFileOperationFinish(RemoveFileOperation operation,
                                              RemoteOperationResult result) {
-        DisplayUtils.showSnackMessage(
-                this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources())
-        );
+
+        if (!operation.isInBackground()) {
+            DisplayUtils.showSnackMessage(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation,
+                    getResources()));
+        }
 
         if (result.isSuccess()) {
             OCFile removedFile = operation.getFile();
             tryStopPlaying(removedFile);
             FileFragment second = getSecondFragment();
-            if (second != null && removedFile.equals(second.getFile())) {
+
+            // check if file is still available, if so do nothing
+            boolean fileAvailable = getStorageManager().fileExists(removedFile.getFileId());
+
+            if (second != null && !fileAvailable && removedFile.equals(second.getFile())) {
                 if (second instanceof PreviewMediaFragment) {
                     ((PreviewMediaFragment) second).stopPreview(true);
                 }
@@ -1785,6 +1795,34 @@ public class FileDisplayActivity extends HookActivity
         }
     }
 
+    private void onRestoreFileVersionOperationFinish(RemoteOperationResult result) {
+        if (result.isSuccess()) {
+            OCFile file = getFile();
+
+            // delete old local copy
+            if (file.isDown()) {
+                List<OCFile> list = new ArrayList<>();
+                list.add(file);
+                getFileOperationsHelper().removeFiles(list, true, true);
+
+                // download new version, only if file was previously download
+                getFileOperationsHelper().syncFile(file);
+            }
+
+            OCFile parent = getStorageManager().getFileById(file.getParentId());
+            startSyncFolderOperation(parent, true, true);
+
+            if (getSecondFragment() instanceof FileDetailFragment) {
+                FileDetailFragment fileDetailFragment = (FileDetailFragment) getSecondFragment();
+                fileDetailFragment.getFileDetailActivitiesFragment().reload();
+            }
+
+            DisplayUtils.showSnackMessage(this, R.string.file_version_restored_successfully);
+        } else {
+            DisplayUtils.showSnackMessage(this, R.string.file_version_restored_error);
+        }
+    }
+
     public void setMediaServiceConnection() {
         mMediaServiceConnection = newMediaConnection();// mediaServiceConnection;
         bindService(new Intent(this, MediaService.class), mMediaServiceConnection, Context.BIND_AUTO_CREATE);
@@ -2082,9 +2120,9 @@ public class FileDisplayActivity extends HookActivity
 
     /**
      * Starts an operation to refresh the requested folder.
-     * <p/>
+     *
      * The operation is run in a new background thread created on the fly.
-     * <p/>
+     *
      * The refresh updates is a "light sync": properties of regular files in folder are updated (including
      * associated shares), but not their contents. Only the contents of files marked to be kept-in-sync are
      * synchronized too.
@@ -2093,7 +2131,25 @@ public class FileDisplayActivity extends HookActivity
      * @param ignoreETag If 'true', the data from the server will be fetched and sync'ed even if the eTag
      *                   didn't change.
      */
-    public void startSyncFolderOperation(final OCFile folder, final boolean ignoreETag) {
+    public void startSyncFolderOperation(OCFile folder, boolean ignoreETag) {
+        startSyncFolderOperation(folder, ignoreETag, false);
+    }
+
+    /**
+     * Starts an operation to refresh the requested folder.
+     *
+     * The operation is run in a new background thread created on the fly.
+     *
+     * The refresh updates is a "light sync": properties of regular files in folder are updated (including
+     * associated shares), but not their contents. Only the contents of files marked to be kept-in-sync are
+     * synchronized too.
+     *
+     * @param folder      Folder to refresh.
+     * @param ignoreETag  If 'true', the data from the server will be fetched and sync'ed even if the eTag
+     *                    didn't change.
+     * @param ignoreFocus reloads file list even without focus, e.g. on tablet mode, focus can still be in detail view
+     */
+    public void startSyncFolderOperation(final OCFile folder, final boolean ignoreETag, boolean ignoreFocus) {
 
         // the execution is slightly delayed to allow the activity get the window focus if it's being started
         // or if the method is called from a dialog that is being dismissed
@@ -2102,7 +2158,7 @@ public class FileDisplayActivity extends HookActivity
                     new Runnable() {
                         @Override
                         public void run() {
-                            if (hasWindowFocus()) {
+                            if (ignoreFocus || hasWindowFocus()) {
                                 long currentSyncTime = System.currentTimeMillis();
                                 mSyncInProgress = true;
 

+ 150 - 0
src/main/java/com/owncloud/android/ui/adapter/ActivityAndVersionListAdapter.java

@@ -0,0 +1,150 @@
+package com.owncloud.android.ui.adapter;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.text.format.DateFormat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.owncloud.android.R;
+import com.owncloud.android.datamodel.FileDataStorageManager;
+import com.owncloud.android.lib.resources.activities.models.Activity;
+import com.owncloud.android.lib.resources.files.FileVersion;
+import com.owncloud.android.ui.interfaces.ActivityListInterface;
+import com.owncloud.android.ui.interfaces.VersionListInterface;
+import com.owncloud.android.utils.DisplayUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+
+public class ActivityAndVersionListAdapter extends ActivityListAdapter {
+
+    private static final int VERSION_TYPE = 102;
+    private VersionListInterface.View versionListInterface;
+
+    public ActivityAndVersionListAdapter(Context context, ActivityListInterface activityListInterface,
+                                         VersionListInterface.View versionListInterface,
+                                         FileDataStorageManager storageManager) {
+        super(context, activityListInterface, storageManager);
+
+        this.versionListInterface = versionListInterface;
+    }
+
+    public void setActivityAndVersionItems(ArrayList<Object> items, boolean clear) {
+        if (clear) {
+            mValues.clear();
+            Collections.sort(items, (o1, o2) -> {
+                long o1Date;
+                long o2Date;
+                if (o1 instanceof Activity) {
+                    o1Date = ((Activity) o1).datetime.getTime();
+                } else {
+                    o1Date = ((FileVersion) o1).getModifiedTimestamp();
+                }
+
+                if (o2 instanceof Activity) {
+                    o2Date = ((Activity) o2).datetime.getTime();
+                } else {
+                    o2Date = ((FileVersion) o2).getModifiedTimestamp();
+                }
+
+                return -1 * Long.compare(o1Date, o2Date);
+            });
+        }
+
+        String sTime = "";
+        for (Object item : items) {
+            String time;
+
+            if (item instanceof Activity) {
+                Activity activity = (Activity) item;
+                if (activity.getDatetime() != null) {
+                    time = getHeaderDateString(context, activity.getDatetime().getTime()).toString();
+                } else if (activity.getDate() != null) {
+                    time = getHeaderDateString(context, activity.getDate().getTime()).toString();
+                } else {
+                    time = context.getString(R.string.date_unknown);
+                }
+            } else {
+                FileVersion version = (FileVersion) item;
+                time = getHeaderDateString(context, version.getModifiedTimestamp()).toString();
+            }
+
+            if (sTime.equalsIgnoreCase(time)) {
+                mValues.add(item);
+            } else {
+                sTime = time;
+                mValues.add(sTime);
+                mValues.add(item);
+            }
+        }
+
+        notifyDataSetChanged();
+    }
+
+    @NonNull
+    @Override
+    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        switch (viewType) {
+            case VERSION_TYPE:
+                View versionView = LayoutInflater.from(parent.getContext()).inflate(R.layout.version_list_item,
+                        parent, false);
+                return new VersionViewHolder(versionView);
+            default:
+                return super.onCreateViewHolder(parent, viewType);
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+        if (holder instanceof VersionViewHolder) {
+            final VersionViewHolder versionViewHolder = (VersionViewHolder) holder;
+            FileVersion fileVersion = (FileVersion) mValues.get(position);
+
+            versionViewHolder.size.setText(DisplayUtils.bytesToHumanReadable(fileVersion.getFileLength()));
+            versionViewHolder.time.setText(DateFormat.format("HH:mm", new Date(fileVersion.getModifiedTimestamp())
+                    .getTime()));
+
+            versionViewHolder.restore.setOnClickListener(v -> versionListInterface.onRestoreClicked(fileVersion));
+        } else {
+            super.onBindViewHolder(holder, position);
+        }
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        Object value = mValues.get(position);
+
+        if (value instanceof Activity)
+            return ACTIVITY_TYPE;
+        else if (value instanceof FileVersion) {
+            return VERSION_TYPE;
+        } else {
+            return HEADER_TYPE;
+        }
+    }
+
+    protected class VersionViewHolder extends RecyclerView.ViewHolder {
+        @BindView(R.id.thumbnail)
+        public ImageView thumbnail;
+        @BindView(R.id.size)
+        public TextView size;
+        @BindView(R.id.time)
+        public TextView time;
+        @BindView(R.id.restore)
+        public ImageView restore;
+
+        VersionViewHolder(View itemView) {
+            super(itemView);
+            ButterKnife.bind(this, itemView);
+        }
+    }
+}

+ 14 - 16
src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java

@@ -24,6 +24,7 @@ import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.drawable.PictureDrawable;
 import android.net.Uri;
+import android.support.annotation.NonNull;
 import android.support.v7.widget.RecyclerView;
 import android.text.Spannable;
 import android.text.SpannableStringBuilder;
@@ -74,19 +75,18 @@ import java.util.List;
 /**
  * Adapter for the activity view
  */
-
 public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
 
-    private static final int HEADER_TYPE = 100;
-    private static final int ACTIVITY_TYPE = 101;
-    private final ActivityListInterface activityListInterface;
+    protected static final int HEADER_TYPE = 100;
+    protected static final int ACTIVITY_TYPE = 101;
+    protected final ActivityListInterface activityListInterface;
     private final int px;
     private static final String TAG = ActivityListAdapter.class.getSimpleName();
     private OwnCloudClient mClient;
 
-    private Context context;
+    protected Context context;
     private FileDataStorageManager storageManager;
-    private List<Object> mValues;
+    protected List<Object> mValues;
 
     public ActivityListAdapter(Context context, ActivityListInterface activityListInterface,
                                FileDataStorageManager storageManager) {
@@ -127,8 +127,9 @@ public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
         notifyDataSetChanged();
     }
 
+    @NonNull
     @Override
-    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
         if (viewType == ACTIVITY_TYPE) {
             View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.activity_list_item, parent, false);
             return new ActivityViewHolder(v);
@@ -136,12 +137,10 @@ public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
             View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.activity_list_item_header, parent, false);
             return new ActivityViewHeaderHolder(v);
         }
-
     }
 
     @Override
-    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
-
+    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
         if (holder instanceof ActivityViewHolder) {
             final ActivityViewHolder activityViewHolder = (ActivityViewHolder) holder;
             Activity activity = (Activity) mValues.get(position);
@@ -354,7 +353,7 @@ public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
         return d.intValue();
     }
 
-    private CharSequence getHeaderDateString(Context context, long modificationTimestamp) {
+    protected CharSequence getHeaderDateString(Context context, long modificationTimestamp) {
         if ((System.currentTimeMillis() - modificationTimestamp) < DateUtils.WEEK_IN_MILLIS) {
             return DisplayUtils.getRelativeDateTimeString(context, modificationTimestamp, DateUtils.DAY_IN_MILLIS,
                     DateUtils.WEEK_IN_MILLIS, 0);
@@ -363,7 +362,7 @@ public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
         }
     }
 
-    private class ActivityViewHolder extends RecyclerView.ViewHolder {
+    protected class ActivityViewHolder extends RecyclerView.ViewHolder {
 
         private final ImageView activityIcon;
         private final TextView subject;
@@ -371,7 +370,7 @@ public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
         private final TextView dateTime;
         private final GridLayout list;
 
-        private ActivityViewHolder(View itemView) {
+        protected ActivityViewHolder(View itemView) {
             super(itemView);
             activityIcon = itemView.findViewById(R.id.activity_icon);
             subject = itemView.findViewById(R.id.activity_subject);
@@ -381,15 +380,14 @@ public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
         }
     }
 
-    private class ActivityViewHeaderHolder extends RecyclerView.ViewHolder {
+    protected class ActivityViewHeaderHolder extends RecyclerView.ViewHolder {
 
         private final TextView title;
 
-        private ActivityViewHeaderHolder(View itemView) {
+        protected ActivityViewHeaderHolder(View itemView) {
             super(itemView);
             title = itemView.findViewById(R.id.title_header);
 
         }
     }
-
 }

+ 7 - 1
src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.java

@@ -37,6 +37,7 @@ public class FileDetailTabAdapter extends FragmentStatePagerAdapter {
     private Account account;
 
     private FileDetailSharingFragment fileDetailSharingFragment;
+    private FileDetailActivitiesFragment fileDetailActivitiesFragment;
 
     public FileDetailTabAdapter(FragmentManager fm, OCFile file, Account account) {
         super(fm);
@@ -49,7 +50,8 @@ public class FileDetailTabAdapter extends FragmentStatePagerAdapter {
     public Fragment getItem(int position) {
         switch (position) {
             case 0:
-                return FileDetailActivitiesFragment.newInstance(file, account);
+                fileDetailActivitiesFragment = FileDetailActivitiesFragment.newInstance(file, account);
+                return fileDetailActivitiesFragment; 
             case 1:
                 fileDetailSharingFragment = FileDetailSharingFragment.newInstance(file, account);
                 return fileDetailSharingFragment;
@@ -62,6 +64,10 @@ public class FileDetailTabAdapter extends FragmentStatePagerAdapter {
         return fileDetailSharingFragment;
     }
 
+    public FileDetailActivitiesFragment getFileDetailActivitiesFragment() {
+        return fileDetailActivitiesFragment;
+    }
+
     @Override
     public int getCount() {
         return 2;

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

@@ -95,7 +95,7 @@ implements ConfirmationDialogFragmentListener {
         if (storageManager.getFileById(mTargetFile.getFileId()) != null) {
             ArrayList<OCFile> list = new ArrayList<>();
             list.add(mTargetFile);
-            cg.getFileOperationsHelper().removeFiles(list, false);
+            cg.getFileOperationsHelper().removeFiles(list, false, false);
         }
     }
     
@@ -107,7 +107,7 @@ implements ConfirmationDialogFragmentListener {
         ComponentsGetter cg = (ComponentsGetter)getActivity();
         ArrayList<OCFile> list = new ArrayList<>();
         list.add(mTargetFile);
-        cg.getFileOperationsHelper().removeFiles(list, true);
+        cg.getFileOperationsHelper().removeFiles(list, true, false);
         
         FileDataStorageManager storageManager = cg.getStorageManager();
         

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

@@ -141,7 +141,7 @@ implements ConfirmationDialogFragmentListener {
     @Override
     public void onConfirmation(String callerTag) {
         ComponentsGetter cg = (ComponentsGetter) getActivity();
-        cg.getFileOperationsHelper().removeFiles(mTargetFiles, false);
+        cg.getFileOperationsHelper().removeFiles(mTargetFiles, false, false);
     }
     
     /**
@@ -150,7 +150,7 @@ implements ConfirmationDialogFragmentListener {
     @Override
     public void onCancel(String callerTag) {
         ComponentsGetter cg = (ComponentsGetter) getActivity();
-        cg.getFileOperationsHelper().removeFiles(mTargetFiles, true);
+        cg.getFileOperationsHelper().removeFiles(mTargetFiles, true, false);
     }
 
     @Override

+ 56 - 11
src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java

@@ -21,6 +21,7 @@
 package com.owncloud.android.ui.fragment;
 
 import android.accounts.Account;
+import android.accounts.AccountManager;
 import android.accounts.AuthenticatorException;
 import android.accounts.OperationCanceledException;
 import android.content.Context;
@@ -52,27 +53,33 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.activities.GetRemoteActivitiesOperation;
 import com.owncloud.android.lib.resources.activities.models.RichObject;
+import com.owncloud.android.lib.resources.files.FileVersion;
+import com.owncloud.android.lib.resources.files.ReadFileVersionsOperation;
+import com.owncloud.android.lib.resources.status.OCCapability;
+import com.owncloud.android.lib.resources.status.OwnCloudVersion;
+import com.owncloud.android.ui.activity.ComponentsGetter;
 import com.owncloud.android.ui.activity.FileActivity;
-import com.owncloud.android.ui.adapter.ActivityListAdapter;
+import com.owncloud.android.ui.adapter.ActivityAndVersionListAdapter;
+import com.owncloud.android.ui.helpers.FileOperationsHelper;
 import com.owncloud.android.ui.interfaces.ActivityListInterface;
+import com.owncloud.android.ui.interfaces.VersionListInterface;
 import com.owncloud.android.utils.ThemeUtils;
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.List;
 
 import butterknife.BindString;
 import butterknife.BindView;
 import butterknife.ButterKnife;
 import butterknife.Unbinder;
 
-public class FileDetailActivitiesFragment extends Fragment implements ActivityListInterface {
+public class FileDetailActivitiesFragment extends Fragment implements ActivityListInterface, VersionListInterface.View {
     private static final String TAG = FileDetailActivitiesFragment.class.getSimpleName();
 
     private static final String ARG_FILE = "FILE";
     private static final String ARG_ACCOUNT = "ACCOUNT";
 
-    private ActivityListAdapter adapter;
+    private ActivityAndVersionListAdapter adapter;
     private Unbinder unbinder;
     private OwnCloudClient ownCloudClient;
 
@@ -111,6 +118,9 @@ public class FileDetailActivitiesFragment extends Fragment implements ActivityLi
 
     @BindString(R.string.activities_no_results_message)
     public String noResultsMessage;
+    private boolean restoreFileVersionSupported;
+    private String userId;
+    private FileOperationsHelper operationsHelper;
 
     public static FileDetailActivitiesFragment newInstance(OCFile file, Account account) {
         FileDetailActivitiesFragment fragment = new FileDetailActivitiesFragment();
@@ -149,6 +159,11 @@ public class FileDetailActivitiesFragment extends Fragment implements ActivityLi
         swipeEmptyListRefreshLayout.setOnRefreshListener(
                 () -> onRefreshListLayout(swipeEmptyListRefreshLayout));
 
+        AccountManager accountManager = AccountManager.get(getContext());
+        userId = accountManager.getUserData(account,
+                com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
+
+
         return view;
     }
 
@@ -175,11 +190,18 @@ public class FileDetailActivitiesFragment extends Fragment implements ActivityLi
 
     private void setupView() {
         FileDataStorageManager storageManager = new FileDataStorageManager(account, getActivity().getContentResolver());
+        operationsHelper = ((ComponentsGetter) getActivity()).getFileOperationsHelper();
+
+        OCCapability capability = storageManager.getCapability(account.name);
+        OwnCloudVersion serverVersion = AccountUtils.getServerVersion(account);
+        restoreFileVersionSupported = capability.getFilesVersioning().isTrue() &&
+                serverVersion.compareTo(OwnCloudVersion.nextcloud_14) >= 0;
+
         emptyContentProgressBar.getIndeterminateDrawable().setColorFilter(ThemeUtils.primaryAccentColor(getContext()),
                 PorterDuff.Mode.SRC_IN);
         emptyContentIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_activity_light_grey));
 
-        adapter = new ActivityListAdapter(getContext(), this, storageManager);
+        adapter = new ActivityAndVersionListAdapter(getContext(), this, this, storageManager);
         recyclerView.setAdapter(adapter);
 
         LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
@@ -205,6 +227,10 @@ public class FileDetailActivitiesFragment extends Fragment implements ActivityLi
         });
     }
 
+    public void reload() {
+        fetchAndSetData(null);
+    }
+
     /**
      * @param pageUrl String
      */
@@ -228,7 +254,7 @@ public class FileDetailActivitiesFragment extends Fragment implements ActivityLi
 
                 GetRemoteActivitiesOperation getRemoteNotificationOperation = new GetRemoteActivitiesOperation(
                         file.getLocalId());
-                
+
                 if (pageUrl != null) {
                     getRemoteNotificationOperation.setNextUrl(pageUrl);
                 }
@@ -236,14 +262,28 @@ public class FileDetailActivitiesFragment extends Fragment implements ActivityLi
                 Log_OC.d(TAG, "BEFORE getRemoteActivitiesOperation.execute");
                 final RemoteOperationResult result = getRemoteNotificationOperation.execute(ownCloudClient);
 
+                ArrayList<Object> versions = null;
+                if (restoreFileVersionSupported) {
+                    ReadFileVersionsOperation readFileVersionsOperation = new ReadFileVersionsOperation(
+                            file.getLocalId(), userId);
+
+                    RemoteOperationResult result1 = readFileVersionsOperation.execute(ownCloudClient);
+
+                    versions = result1.getData();
+                }
+
                 if (result.isSuccess() && result.getData() != null) {
                     final ArrayList<Object> data = result.getData();
-                    final ArrayList<Object> activities = (ArrayList) data.get(0);
+                    final ArrayList<Object> activitiesAndVersions = (ArrayList) data.get(0);
+
+                    if (restoreFileVersionSupported && versions != null) {
+                        activitiesAndVersions.addAll(versions);
+                    }
                     nextPageUrl = (String) data.get(1);
 
                     activity.runOnUiThread(() -> {
-                        populateList(activities, ownCloudClient, pageUrl == null);
-                        if (activities.isEmpty()) {
+                        populateList(activitiesAndVersions, pageUrl == null);
+                        if (activitiesAndVersions.isEmpty()) {
                             setEmptyContent(noResultsHeadline, noResultsMessage);
                             list.setVisibility(View.GONE);
                             empty.setVisibility(View.VISIBLE);
@@ -283,8 +323,8 @@ public class FileDetailActivitiesFragment extends Fragment implements ActivityLi
         t.start();
     }
 
-    private void populateList(List<Object> activities, OwnCloudClient mClient, boolean clear) {
-        adapter.setActivityItems(activities, mClient, clear);
+    private void populateList(ArrayList<Object> activities, boolean clear) {
+        adapter.setActivityAndVersionItems(activities, clear);
     }
 
     private void setEmptyContent(String headline, String message) {
@@ -344,4 +384,9 @@ public class FileDetailActivitiesFragment extends Fragment implements ActivityLi
         outState.putParcelable(FileActivity.EXTRA_FILE, file);
         outState.putParcelable(FileActivity.EXTRA_ACCOUNT, account);
     }
+
+    @Override
+    public void onRestoreClicked(FileVersion fileVersion) {
+        operationsHelper.restoreFileVersion(fileVersion, userId);
+    }
 }

+ 9 - 0
src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java

@@ -183,6 +183,15 @@ public class FileDetailFragment extends FileFragment implements OnClickListener
         return ((FileDetailTabAdapter)viewPager.getAdapter()).getFileDetailSharingFragment();
     }
 
+    /**
+     * return the reference to the file detail activity fragment to communicate with it.
+     *
+     * @return reference to the {@link FileDetailActivitiesFragment}
+     */
+    public FileDetailActivitiesFragment getFileDetailActivitiesFragment() {
+        return ((FileDetailTabAdapter) viewPager.getAdapter()).getFileDetailActivitiesFragment();
+    }
+
     @Override
     public void onResume() {
         super.onResume();

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

@@ -53,6 +53,7 @@ import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.CheckEtagOperation;
+import com.owncloud.android.lib.resources.files.FileVersion;
 import com.owncloud.android.lib.resources.shares.OCShare;
 import com.owncloud.android.lib.resources.shares.ShareType;
 import com.owncloud.android.lib.resources.status.OwnCloudVersion;
@@ -426,6 +427,28 @@ public class FileOperationsHelper {
         }
     }
 
+    /**
+     * Helper method to revert to a file version. Starts a request to do it in {@link OperationsService}
+     *
+     * @param fileVersion The file version to restore
+     * @param userId      userId of current account
+     */
+    public void restoreFileVersion(FileVersion fileVersion, String userId) {
+        if (fileVersion != null) {
+            mFileActivity.showLoadingDialog(mFileActivity.getApplicationContext().
+                    getString(R.string.wait_a_moment));
+
+            Intent service = new Intent(mFileActivity, OperationsService.class);
+            service.setAction(OperationsService.ACTION_RESTORE_VERSION);
+            service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
+            service.putExtra(OperationsService.EXTRA_FILE_VERSION, fileVersion);
+            service.putExtra(OperationsService.EXTRA_USER_ID, userId);
+            mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service);
+        } else {
+            Log_OC.e(TAG, "Trying to restore a NULL FileVersion");
+        }
+    }
+
 
     /**
      * @return 'True' if the server supports the Share API
@@ -836,8 +859,9 @@ public class FileOperationsHelper {
      * @param files         Files to delete
      * @param onlyLocalCopy When 'true' only local copy of the files is removed; otherwise files are also deleted
      *                      in the server.
+     * @param inBackground  When 'true', do not show any loading dialog
      */
-    public void removeFiles(Collection<OCFile> files, boolean onlyLocalCopy) {
+    public void removeFiles(Collection<OCFile> files, boolean onlyLocalCopy, boolean inBackground) {
         for (OCFile file : files) {
             // RemoveFile
             Intent service = new Intent(mFileActivity, OperationsService.class);
@@ -845,10 +869,13 @@ public class FileOperationsHelper {
             service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
             service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
             service.putExtra(OperationsService.EXTRA_REMOVE_ONLY_LOCAL, onlyLocalCopy);
+            service.putExtra(OperationsService.EXTRA_IN_BACKGROUND, inBackground);
             mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service);
         }
 
-        mFileActivity.showLoadingDialog(mFileActivity.getString(R.string.wait_a_moment));
+        if (!inBackground) {
+            mFileActivity.showLoadingDialog(mFileActivity.getString(R.string.wait_a_moment));
+        }
     }
 
 

+ 31 - 0
src/main/java/com/owncloud/android/ui/interfaces/VersionListInterface.java

@@ -0,0 +1,31 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2018 Tobias Kaminsky
+ * Copyright (C) 2018 Nextcloud GmbH.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.interfaces;
+
+import com.owncloud.android.lib.resources.files.FileVersion;
+
+public interface VersionListInterface {
+
+    interface View {
+        void onRestoreClicked(FileVersion fileVersion);
+    }
+}

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

@@ -696,6 +696,16 @@ public class DisplayUtils {
         Snackbar.make(view, messageResource, Snackbar.LENGTH_LONG).show();
     }
 
+    /**
+     * Show a temporary message in a {@link Snackbar} bound to the given view.
+     *
+     * @param view    The view the {@link Snackbar} is bound to.
+     * @param message The message.
+     */
+    public static void showSnackMessage(View view, String message) {
+        Snackbar.make(view, message, Snackbar.LENGTH_LONG).show();
+    }
+
     /**
      * create a temporary message in a {@link Snackbar} bound to the given view.
      *

+ 11 - 0
src/main/res/drawable/ic_history.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="16dp"
+        android:height="16dp"
+        android:viewportWidth="16"
+        android:viewportHeight="16">
+
+    <path
+        android:fillColor="#000000"
+        android:pathData="M9.025 1.08c-3.95 0-6.535 3.447-6.364 6.72h-2.161l3.904 3.92 4.08-3.874h-2.147c-0.237-1.7 1.163-3.114 2.689-3.092 1.595 0.024 2.8 1.23 2.8 2.734 0.09 1.594-1.63 3.428-3.966 2.53 0 1.23 0.003 2.545 0 3.765 4.19 0.83 7.64-2.51 7.64-6.25 0-3.563-2.92-6.453-6.475-6.453z"/>
+</vector>

+ 89 - 0
src/main/res/layout/version_list_item.xml

@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Nextcloud Android client application
+
+  @author Tobias Kaminsky
+  Copyright (C) 2018 Tobias Kaminsky
+  Copyright (C) 2018 Nextcloud GmbH.
+ 
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+ 
+  You should have received a copy of the GNU General Public License
+  along with this program. If not, see <https://www.gnu.org/licenses/>.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:paddingTop="@dimen/standard_padding"
+    android:paddingRight="@dimen/standard_padding"
+    android:paddingBottom="@dimen/standard_padding"
+    android:paddingLeft="@dimen/standard_padding">
+
+    <ImageView
+        android:id="@+id/thumbnail"
+        android:layout_width="@dimen/activity_icon_width"
+        android:layout_height="@dimen/activity_icon_height"
+        android:layout_marginEnd="@dimen/activity_icon_layout_right_end_margin"
+        android:layout_marginRight="@dimen/activity_icon_layout_right_end_margin"
+        android:alpha="0.5"
+        android:contentDescription="@string/thumbnail"
+        android:src="@drawable/ic_activity"/>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/version_created"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:text="@string/new_version_was_created"
+            android:textAppearance="?android:attr/textAppearanceListItem"/>
+
+        <TextView
+            android:id="@+id/size"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:ellipsize="end"
+            tools:text="256 KB"
+            android:textColor="?android:attr/textColorSecondary"/>
+    </LinearLayout>
+
+    <ImageView
+        android:id="@+id/restore"
+        android:layout_width="@dimen/restore_icon_width"
+        android:layout_height="@dimen/restore_icon_height"
+        android:layout_marginEnd="@dimen/restore_icon_layout_right_end_margin"
+        android:layout_marginRight="@dimen/restore_icon_layout_right_end_margin"
+        android:layout_weight="1"
+        android:alpha="0.5"
+        android:contentDescription="@string/restore"
+        android:src="@drawable/ic_history"/>
+
+    <TextView
+        android:id="@+id/time"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="end"
+        android:layout_weight="1"
+        android:ellipsize="end"
+        android:textAlignment="textEnd"
+        tools:text="13:24"
+        android:textColor="?android:attr/textColorSecondary"/>
+
+</LinearLayout>

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

@@ -88,6 +88,9 @@
     <dimen name="media_grid_spacing">2dp</dimen>
     <dimen name="account_item_layout_user_image_left_start_margin">12dp</dimen>
     <dimen name="account_item_layout_user_image_margin">1dp</dimen>
+    <dimen name="restore_icon_width">24dp</dimen>
+    <dimen name="restore_icon_height">24dp</dimen>
+    <dimen name="restore_icon_layout_right_end_margin">24dp</dimen>
     <dimen name="activity_icon_width">32dp</dimen>
     <dimen name="activity_icon_height">32dp</dimen>
     <dimen name="activity_icon_layout_right_end_margin">24dp</dimen>

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

@@ -797,4 +797,8 @@
     <string name="updating_share_failed">Updating share failed</string>
     <string name="whats_new_device_credentials_title">Use Android device protection</string>
     <string name="whats_new_device_credentials_content">Use anything like a pattern, password, pin or your fingerprint to keep your data safe.</string>
+    <string name="restore">Restore file</string>
+    <string name="new_version_was_created">New version was created</string>
+    <string name="file_version_restored_successfully">Successfully restored file version.</string>
+    <string name="file_version_restored_error">Error restoring file version!</string>
 </resources>

+ 3 - 3
src/test/java/com/owncloud/android/utils/ErrorMessageAdapterUnitTest.java

@@ -1,4 +1,4 @@
-/**
+/*
  *   ownCloud Android client application
  *
  *   @author David A. Velasco
@@ -58,7 +58,7 @@ public class ErrorMessageAdapterUnitTest {
     private final static String ACCOUNT_TYPE = "nextcloud";
 
     @Mock
-    Resources mMockResources;
+    private Resources mMockResources;
 
     @Test
     public void getErrorCauseMessageForForbiddenRemoval() {
@@ -73,7 +73,7 @@ public class ErrorMessageAdapterUnitTest {
         // ... when method under test is called ...
         String errorMessage = ErrorMessageAdapter.getErrorCauseMessage(
             new RemoteOperationResult(RemoteOperationResult.ResultCode.FORBIDDEN),
-                new RemoveFileOperation(PATH_TO_DELETE, false, account, MainApp.getAppContext()),
+                new RemoveFileOperation(PATH_TO_DELETE, false, account, false, MainApp.getAppContext()),
             mMockResources
         );