Explorar o código

Add image editor

Signed-off-by: ZetaTom <70907959+ZetaTom@users.noreply.github.com>
ZetaTom hai 1 ano
pai
achega
c113861e63

+ 3 - 0
app/build.gradle

@@ -315,6 +315,9 @@ dependencies {
     implementation "io.noties:prism4j:$prismVersion"
     implementation "io.noties:prism4j:$prismVersion"
     kapt "io.noties:prism4j-bundler:$prismVersion"
     kapt "io.noties:prism4j-bundler:$prismVersion"
 
 
+    // dependencies for image cropping and rotation
+    implementation 'com.vanniktech:android-image-cropper:4.5.0'
+
     implementation('org.mnode.ical4j:ical4j:3.0.0') {
     implementation('org.mnode.ical4j:ical4j:3.0.0') {
         ['org.apache.commons', 'commons-logging'].each {
         ['org.apache.commons', 'commons-logging'].each {
             exclude group: "$it"
             exclude group: "$it"

+ 4 - 0
app/src/main/AndroidManifest.xml

@@ -487,6 +487,10 @@
             android:name="com.nextcloud.client.documentscan.DocumentScanActivity"
             android:name="com.nextcloud.client.documentscan.DocumentScanActivity"
             android:exported="false"
             android:exported="false"
             android:theme="@style/Theme.ownCloud.Toolbar" />
             android:theme="@style/Theme.ownCloud.Toolbar" />
+        <activity
+            android:name="com.nextcloud.client.editimage.EditImageActivity"
+            android:exported="false"
+            android:theme="@style/Theme.ownCloud.Toolbar" />
 
 
         <activity
         <activity
             android:name="com.nmc.android.ui.LauncherActivity"
             android:name="com.nmc.android.ui.LauncherActivity"

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

@@ -21,6 +21,7 @@
 package com.nextcloud.client.di;
 package com.nextcloud.client.di;
 
 
 import com.nextcloud.client.documentscan.DocumentScanActivity;
 import com.nextcloud.client.documentscan.DocumentScanActivity;
+import com.nextcloud.client.editimage.EditImageActivity;
 import com.nextcloud.client.etm.EtmActivity;
 import com.nextcloud.client.etm.EtmActivity;
 import com.nextcloud.client.files.downloader.FileTransferService;
 import com.nextcloud.client.files.downloader.FileTransferService;
 import com.nextcloud.client.jobs.NotificationWork;
 import com.nextcloud.client.jobs.NotificationWork;
@@ -471,4 +472,7 @@ abstract class ComponentsModule {
     @ContributesAndroidInjector
     @ContributesAndroidInjector
     abstract LauncherActivity launcherActivity();
     abstract LauncherActivity launcherActivity();
 
 
+    @ContributesAndroidInjector
+    abstract EditImageActivity editImageActivity();
+
 }
 }

+ 175 - 0
app/src/main/java/com/nextcloud/client/editimage/EditImageActivity.kt

@@ -0,0 +1,175 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author ZetaTom
+ * Copyright (C) 2023 ZetaTom
+ * Copyright (C) 2023 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.nextcloud.client.editimage
+
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.DrawableCompat
+import com.canhub.cropper.CropImageView
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.R
+import com.owncloud.android.databinding.ActivityEditImageBinding
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.files.services.FileUploader
+import com.owncloud.android.files.services.NameCollisionPolicy
+import com.owncloud.android.lib.common.operations.OnRemoteOperationListener
+import com.owncloud.android.operations.UploadFileOperation
+import com.owncloud.android.ui.activity.FileActivity
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.FilesUploadHelper
+import com.owncloud.android.utils.MimeType
+import java.io.File
+
+class EditImageActivity: FileActivity(),
+    OnRemoteOperationListener,
+    CropImageView.OnSetImageUriCompleteListener,
+    CropImageView.OnCropImageCompleteListener,
+    Injectable {
+
+    private lateinit var binding: ActivityEditImageBinding
+    private lateinit var file: OCFile
+    private lateinit var format: Bitmap.CompressFormat
+
+    companion object {
+        const val EXTRA_FILE = "FILE"
+        const val OPEN_IMAGE_EDITOR = "OPEN_IMAGE_EDITOR"
+
+        private val supportedMimeTypes = arrayOf(
+            MimeType.PNG,
+            MimeType.JPEG,
+            MimeType.WEBP,
+            MimeType.TIFF,
+            MimeType.BMP,
+            MimeType.HEIC
+        )
+
+        fun canBePreviewed(file: OCFile): Boolean {
+            return file.mimeType in supportedMimeTypes
+        }
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        binding = ActivityEditImageBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        file = intent.extras?.getParcelable(EXTRA_FILE) ?: throw IllegalArgumentException("Missing file argument")
+
+        setSupportActionBar(binding.toolbar)
+        supportActionBar?.apply {
+            title = file.fileName
+            setDisplayHomeAsUpEnabled(true)
+        }
+
+        window.statusBarColor = ContextCompat.getColor(this, R.color.black)
+        window.navigationBarColor = getColor(R.color.black)
+        window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
+
+        setupCropper()
+    }
+
+    override fun onCropImageComplete(view: CropImageView, result: CropImageView.CropResult) {
+        if (!result.isSuccessful) {
+            DisplayUtils.showSnackMessage(this, "Unable to edit image.")
+            return
+        }
+        val resultUri = result.getUriFilePath(this, false)
+        val newFileName = file.fileName.substring(0, file.fileName.lastIndexOf('.')) +
+            " edited" +
+            resultUri?.substring(resultUri.lastIndexOf('.'))
+
+        FilesUploadHelper().uploadNewFiles(
+            user = storageManager.user,
+            localPaths = arrayOf(resultUri!!),
+            remotePaths = arrayOf(file.parentRemotePath + File.separator + newFileName),
+            createRemoteFolder = false,
+            createdBy = UploadFileOperation.CREATED_BY_USER,
+            requiresWifi = false,
+            requiresCharging = false,
+            nameCollisionPolicy = NameCollisionPolicy.RENAME,
+            localBehavior = FileUploader.LOCAL_BEHAVIOUR_DELETE
+        )
+    }
+
+    override fun onSetImageUriComplete(view: CropImageView, uri: Uri, error: Exception?) {
+        if (error != null) {
+            DisplayUtils.showSnackMessage(this, "Unable to edit image.")
+            return
+        }
+        view.visibility = View.VISIBLE
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+        menuInflater.inflate(R.menu.custom_menu_placeholder, menu)
+        val saveIcon = AppCompatResources.getDrawable(this, R.drawable.ic_check)
+        if (saveIcon != null) {
+            DrawableCompat.setTint(saveIcon, resources.getColor(R.color.white, theme))
+        }
+        menu?.findItem(R.id.custom_menu_placeholder_item)?.setIcon(saveIcon)
+        return true
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+        R.id.custom_menu_placeholder_item -> {
+            binding.cropImageView.croppedImageAsync(format)
+            finish()
+            true
+        }
+        else -> {
+            finish()
+            true
+        }
+    }
+
+    private fun setupCropper() {
+        val cropper = binding.cropImageView
+
+        binding.rotateLeft.setOnClickListener {
+            cropper.rotateImage(-90)
+        }
+        binding.rotateRight.setOnClickListener {
+            cropper.rotateImage(90)
+        }
+        binding.flipVertical.setOnClickListener {
+            cropper.flipImageVertically()
+        }
+        binding.flipHorizontal.setOnClickListener {
+            cropper.flipImageHorizontally()
+        }
+
+        cropper.setOnSetImageUriCompleteListener(this)
+        cropper.setOnCropImageCompleteListener(this)
+        cropper.setImageUriAsync(file.storageUri)
+
+        format = when (file.mimeType) {
+            MimeType.PNG -> Bitmap.CompressFormat.PNG
+            MimeType.WEBP -> Bitmap.CompressFormat.WEBP
+            else -> Bitmap.CompressFormat.JPEG
+        }
+    }
+}

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

@@ -30,6 +30,7 @@ import android.view.Menu;
 
 
 import com.nextcloud.android.files.FileLockingHelper;
 import com.nextcloud.android.files.FileLockingHelper;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.User;
+import com.nextcloud.client.editimage.EditImageActivity;
 import com.nextcloud.utils.EditorUtils;
 import com.nextcloud.utils.EditorUtils;
 import com.owncloud.android.R;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.FileDataStorageManager;
@@ -282,7 +283,8 @@ public class FileMenuFilter {
 
 
         String mimeType = files.iterator().next().getMimeType();
         String mimeType = files.iterator().next().getMimeType();
 
 
-        if (!isRichDocumentEditingSupported(capability, mimeType) && !editorUtils.isEditorAvailable(user, mimeType)) {
+        if (!isRichDocumentEditingSupported(capability, mimeType) && !editorUtils.isEditorAvailable(user, mimeType) &&
+            !(isSingleImage() && EditImageActivity.Companion.canBePreviewed(files.iterator().next()))) {
             toHide.add(R.id.action_edit);
             toHide.add(R.id.action_edit);
         }
         }
     }
     }

+ 36 - 12
app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -62,6 +62,7 @@ import com.nextcloud.client.account.User;
 import com.nextcloud.client.appinfo.AppInfo;
 import com.nextcloud.client.appinfo.AppInfo;
 import com.nextcloud.client.core.AsyncRunner;
 import com.nextcloud.client.core.AsyncRunner;
 import com.nextcloud.client.di.Injectable;
 import com.nextcloud.client.di.Injectable;
+import com.nextcloud.client.editimage.EditImageActivity;
 import com.nextcloud.client.files.DeepLinkHandler;
 import com.nextcloud.client.files.DeepLinkHandler;
 import com.nextcloud.client.media.PlayerServiceConnection;
 import com.nextcloud.client.media.PlayerServiceConnection;
 import com.nextcloud.client.network.ClientFactory;
 import com.nextcloud.client.network.ClientFactory;
@@ -1511,18 +1512,20 @@ public class FileDisplayActivity extends FileActivity
             if (mWaitingToSend != null) {
             if (mWaitingToSend != null) {
                 // update file after downloading
                 // update file after downloading
                 mWaitingToSend = getStorageManager().getFileByRemoteId(mWaitingToSend.getRemoteId());
                 mWaitingToSend = getStorageManager().getFileByRemoteId(mWaitingToSend.getRemoteId());
-                if (mWaitingToSend != null && mWaitingToSend.isDown() && downloadBehaviour != null) {
-                    switch (downloadBehaviour) {
-                        case OCFileListFragment.DOWNLOAD_SEND:
-                            String packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME);
-                            String activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME);
-
-                            sendDownloadedFile(packageName, activityName);
-                            break;
-                        default:
-                            // do nothing
-                            break;
-                    }
+                if (mWaitingToSend != null && mWaitingToSend.isDown() &&
+                    downloadBehaviour.equals(OCFileListFragment.DOWNLOAD_SEND)) {
+                    String packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME);
+                    String activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME);
+
+                    sendDownloadedFile(packageName, activityName);
+                }
+            }
+
+            if (mWaitingToPreview != null && downloadBehaviour != null) {
+                mWaitingToPreview = getStorageManager().getFileByRemoteId(mWaitingToPreview.getRemoteId());
+                if (mWaitingToPreview != null && mWaitingToPreview.isDown() &&
+                    downloadBehaviour.equals(EditImageActivity.OPEN_IMAGE_EDITOR)) {
+                    startImageEditor(mWaitingToPreview);
                 }
                 }
             }
             }
         }
         }
@@ -2276,6 +2279,27 @@ public class FileDisplayActivity extends FileActivity
     }
     }
 
 
 
 
+    /**
+     * Opens EditImageActivity with given file loaded. If file is not available locally, it will be synced before
+     * opening the image editor.
+     *
+     * @param file      {@link OCFile} (image) to be loaded into image editor
+     */
+    public void startImageEditor(OCFile file) {
+        if (file.isDown()) {
+            Intent editImageIntent = new Intent(this, EditImageActivity.class);
+            editImageIntent.putExtra(EditImageActivity.EXTRA_FILE, file);
+            startActivity(editImageIntent);
+        } else {
+            mWaitingToPreview = file;
+            requestForDownload(file,EditImageActivity.OPEN_IMAGE_EDITOR, getPackageName(),
+                               this.getClass().getSimpleName());
+            updateActionBarTitleAndHomeButton(file);
+            setFile(file);
+        }
+    }
+
+
     /**
     /**
      * Request stopping the upload/download operation in progress over the given {@link OCFile} file.
      * Request stopping the upload/download operation in progress over the given {@link OCFile} file.
      *
      *

+ 3 - 0
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -56,6 +56,7 @@ import com.nextcloud.client.device.DeviceInfo;
 import com.nextcloud.client.di.Injectable;
 import com.nextcloud.client.di.Injectable;
 import com.nextcloud.client.documentscan.AppScanOptionalFeature;
 import com.nextcloud.client.documentscan.AppScanOptionalFeature;
 import com.nextcloud.client.documentscan.DocumentScanActivity;
 import com.nextcloud.client.documentscan.DocumentScanActivity;
+import com.nextcloud.client.editimage.EditImageActivity;
 import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.network.ClientFactory;
 import com.nextcloud.client.network.ClientFactory;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.client.preferences.AppPreferences;
@@ -1168,6 +1169,8 @@ public class OCFileListFragment extends ExtendedListFragment implements
                 if (editorUtils.isEditorAvailable(accountManager.getUser(),
                 if (editorUtils.isEditorAvailable(accountManager.getUser(),
                                                   singleFile.getMimeType())) {
                                                   singleFile.getMimeType())) {
                     mContainerActivity.getFileOperationsHelper().openFileWithTextEditor(singleFile, getContext());
                     mContainerActivity.getFileOperationsHelper().openFileWithTextEditor(singleFile, getContext());
+                } else if (EditImageActivity.Companion.canBePreviewed(singleFile)) {
+                    ((FileDisplayActivity) mContainerActivity).startImageEditor(singleFile);
                 } else {
                 } else {
                     mContainerActivity.getFileOperationsHelper().openFileAsRichDocument(singleFile, getContext());
                     mContainerActivity.getFileOperationsHelper().openFileAsRichDocument(singleFile, getContext());
                 }
                 }

+ 62 - 16
app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.java

@@ -29,7 +29,6 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.ServiceConnection;
-import android.graphics.Color;
 import android.os.Bundle;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.IBinder;
 import android.view.MenuItem;
 import android.view.MenuItem;
@@ -37,6 +36,7 @@ import android.view.View;
 
 
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.di.Injectable;
 import com.nextcloud.client.di.Injectable;
+import com.nextcloud.client.editimage.EditImageActivity;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.java.util.Optional;
 import com.nextcloud.java.util.Optional;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.MainApp;
@@ -57,9 +57,11 @@ import com.owncloud.android.operations.SynchronizeFileOperation;
 import com.owncloud.android.ui.activity.FileActivity;
 import com.owncloud.android.ui.activity.FileActivity;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
 import com.owncloud.android.ui.fragment.FileFragment;
 import com.owncloud.android.ui.fragment.FileFragment;
+import com.owncloud.android.ui.fragment.OCFileListFragment;
 import com.owncloud.android.utils.MimeTypeUtil;
 import com.owncloud.android.utils.MimeTypeUtil;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
+import java.util.Objects;
 
 
 import javax.inject.Inject;
 import javax.inject.Inject;
 
 
@@ -92,6 +94,7 @@ public class PreviewImageActivity extends FileActivity implements
     private boolean mHasSavedPosition;
     private boolean mHasSavedPosition;
     private boolean mRequestWaitingForBinder;
     private boolean mRequestWaitingForBinder;
     private DownloadFinishReceiver mDownloadFinishReceiver;
     private DownloadFinishReceiver mDownloadFinishReceiver;
+    private UploadFinishReceiver mUploadFinishReceiver;
     private View mFullScreenAnchorView;
     private View mFullScreenAnchorView;
     @Inject AppPreferences preferences;
     @Inject AppPreferences preferences;
     @Inject LocalBroadcastManager localBroadcastManager;
     @Inject LocalBroadcastManager localBroadcastManager;
@@ -324,10 +327,15 @@ public class PreviewImageActivity extends FileActivity implements
         super.onResume();
         super.onResume();
 
 
         mDownloadFinishReceiver = new DownloadFinishReceiver();
         mDownloadFinishReceiver = new DownloadFinishReceiver();
+        IntentFilter downloadIntentFilter = new IntentFilter(FileDownloader.getDownloadFinishMessage());
+        downloadIntentFilter.addAction(FileDownloader.getDownloadAddedMessage());
+        localBroadcastManager.registerReceiver(mDownloadFinishReceiver, downloadIntentFilter);
 
 
-        IntentFilter filter = new IntentFilter(FileDownloader.getDownloadFinishMessage());
-        filter.addAction(FileDownloader.getDownloadAddedMessage());
-        localBroadcastManager.registerReceiver(mDownloadFinishReceiver, filter);
+
+        mUploadFinishReceiver = new UploadFinishReceiver();
+        IntentFilter uploadIntentFilter = new IntentFilter(FileUploader.getUploadFinishMessage());
+        uploadIntentFilter.addAction(FileUploader.getUploadFinishMessage());
+        localBroadcastManager.registerReceiver(mUploadFinishReceiver, uploadIntentFilter);
     }
     }
 
 
     @Override
     @Override
@@ -367,6 +375,10 @@ public class PreviewImageActivity extends FileActivity implements
     }
     }
 
 
     public void requestForDownload(OCFile file) {
     public void requestForDownload(OCFile file) {
+        requestForDownload(file, null);
+    }
+
+    public void requestForDownload(OCFile file, String downloadBehaviour) {
         if (mDownloaderBinder == null) {
         if (mDownloaderBinder == null) {
             Log_OC.d(TAG, "requestForDownload called without binder to download service");
             Log_OC.d(TAG, "requestForDownload called without binder to download service");
 
 
@@ -375,6 +387,9 @@ public class PreviewImageActivity extends FileActivity implements
             Intent i = new Intent(this, FileDownloader.class);
             Intent i = new Intent(this, FileDownloader.class);
             i.putExtra(FileDownloader.EXTRA_USER, user);
             i.putExtra(FileDownloader.EXTRA_USER, user);
             i.putExtra(FileDownloader.EXTRA_FILE, file);
             i.putExtra(FileDownloader.EXTRA_FILE, file);
+            if (downloadBehaviour != null) {
+                i.putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, downloadBehaviour);
+            }
             startService(i);
             startService(i);
         }
         }
     }
     }
@@ -448,18 +463,35 @@ public class PreviewImageActivity extends FileActivity implements
     private class DownloadFinishReceiver extends BroadcastReceiver {
     private class DownloadFinishReceiver extends BroadcastReceiver {
         @Override
         @Override
         public void onReceive(Context context, Intent intent) {
         public void onReceive(Context context, Intent intent) {
-            String accountName = intent.getStringExtra(FileDownloader.ACCOUNT_NAME);
-            String downloadedRemotePath = intent.getStringExtra(FileDownloader.EXTRA_REMOTE_PATH);
-            if (getAccount().name.equals(accountName) &&
-                    downloadedRemotePath != null) {
+            previewNewImage(intent);
+        }
+    }
 
 
-                OCFile file = getStorageManager().getFileByPath(downloadedRemotePath);
-                int position = mPreviewImagePagerAdapter.getFilePosition(file);
-                boolean downloadWasFine = intent.getBooleanExtra(FileDownloader.EXTRA_DOWNLOAD_RESULT, false);
-                //boolean isOffscreen =  Math.abs((mViewPager.getCurrentItem() - position))
-                // <= mViewPager.getOffscreenPageLimit();
+    private class UploadFinishReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            previewNewImage(intent);
+        }
+    }
 
 
-                if (position >= 0 && intent.getAction().equals(FileDownloader.getDownloadFinishMessage())) {
+    private void previewNewImage(Intent intent) {
+        if (!Objects.equals(intent.getAction(), FileDownloader.getDownloadFinishMessage()) &&
+            !Objects.equals(intent.getAction(), FileUploader.getUploadFinishMessage()) ) {
+            return;
+        }
+
+        String accountName = intent.getStringExtra(FileDownloader.ACCOUNT_NAME);
+        String downloadedRemotePath = intent.getStringExtra(FileDownloader.EXTRA_REMOTE_PATH);
+        String downloadBehaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR);
+        if (getAccount().name.equals(accountName) && downloadedRemotePath != null) {
+            OCFile file = getStorageManager().getFileByPath(downloadedRemotePath);
+            boolean downloadWasFine = intent.getBooleanExtra(FileDownloader.EXTRA_DOWNLOAD_RESULT, false);
+
+            if (Objects.equals(downloadBehaviour, EditImageActivity.OPEN_IMAGE_EDITOR)) {
+                startImageEditor(file);
+            } else {
+                int position = mPreviewImagePagerAdapter.getFilePosition(file);
+                if (position >= 0) {
                     if (downloadWasFine) {
                     if (downloadWasFine) {
                         mPreviewImagePagerAdapter.updateFile(position, file);
                         mPreviewImagePagerAdapter.updateFile(position, file);
 
 
@@ -467,8 +499,12 @@ public class PreviewImageActivity extends FileActivity implements
                         mPreviewImagePagerAdapter.updateWithDownloadError(position);
                         mPreviewImagePagerAdapter.updateWithDownloadError(position);
                     }
                     }
                     mPreviewImagePagerAdapter.notifyDataSetChanged();   // will trigger the creation of new fragments
                     mPreviewImagePagerAdapter.notifyDataSetChanged();   // will trigger the creation of new fragments
-                } else {
-                    Log_OC.d(TAG, "Download finished, but the fragment is offscreen");
+                } else if (downloadWasFine) {
+                    initViewPager(getUser().get());
+                    int newPosition = mPreviewImagePagerAdapter.getFilePosition(file);
+                    if (newPosition >= 0) {
+                        mViewPager.setCurrentItem(newPosition);
+                    }
                 }
                 }
             }
             }
         }
         }
@@ -497,6 +533,16 @@ public class PreviewImageActivity extends FileActivity implements
         hideSystemUI(mFullScreenAnchorView);
         hideSystemUI(mFullScreenAnchorView);
     }
     }
 
 
+    public void startImageEditor(OCFile file) {
+        if (file.isDown()) {
+            Intent editImageIntent = new Intent(this, EditImageActivity.class);
+            editImageIntent.putExtra(EditImageActivity.EXTRA_FILE, file);
+            startActivity(editImageIntent);
+        } else {
+            requestForDownload(file, EditImageActivity.OPEN_IMAGE_EDITOR);
+        }
+    }
+
     @Override
     @Override
     public void onBrowsedDownTo(OCFile folder) {
     public void onBrowsedDownTo(OCFile folder) {
         // TODO Auto-generated method stub
         // TODO Auto-generated method stub

+ 2 - 0
app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.java

@@ -422,6 +422,8 @@ public class PreviewImageFragment extends FileFragment implements Injectable {
                                                                     getContext(),
                                                                     getContext(),
                                                                     getView(),
                                                                     getView(),
                                                                     backgroundJobManager);
                                                                     backgroundJobManager);
+        } else if (itemId == R.id.action_edit) {
+            ((PreviewImageActivity) requireActivity()).startImageEditor(getFile());
         }
         }
     }
     }
 
 

+ 4 - 0
app/src/main/java/com/owncloud/android/utils/MimeType.java

@@ -25,6 +25,10 @@ public final class MimeType {
     public static final String WEBDAV_FOLDER = "httpd/unix-directory";
     public static final String WEBDAV_FOLDER = "httpd/unix-directory";
     public static final String JPEG = "image/jpeg";
     public static final String JPEG = "image/jpeg";
     public static final String TIFF = "image/tiff";
     public static final String TIFF = "image/tiff";
+    public static final String PNG = "image/png";
+    public static final String WEBP = "image/webp";
+    public static final String BMP = "image/bmp";
+    public static final String HEIC = "image/heic";
     public static final String TEXT_PLAIN = "text/plain";
     public static final String TEXT_PLAIN = "text/plain";
     public static final String FILE = "application/octet-stream";
     public static final String FILE = "application/octet-stream";
     public static final String PDF = "application/pdf";
     public static final String PDF = "application/pdf";

+ 5 - 0
app/src/main/res/drawable/outline_flip_24.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M15,21h2v-2h-2v2zM19,9h2L21,7h-2v2zM3,5v14c0,1.1 0.9,2 2,2h4v-2L5,19L5,5h4L9,3L5,3c-1.1,0 -2,0.9 -2,2zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM11,23h2L13,1h-2v22zM19,17h2v-2h-2v2zM15,5h2L17,3h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2z"/>
+</vector>

+ 5 - 0
app/src/main/res/drawable/outline_rotate_90_degrees_ccw_24.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M7.34,6.41L0.86,12.9l6.49,6.48 6.49,-6.48 -6.5,-6.49zM3.69,12.9l3.66,-3.66L11,12.9l-3.66,3.66 -3.65,-3.66zM19.36,6.64C17.61,4.88 15.3,4 13,4L13,0.76L8.76,5 13,9.24L13,6c1.79,0 3.58,0.68 4.95,2.05 2.73,2.73 2.73,7.17 0,9.9C16.58,19.32 14.79,20 13,20c-0.97,0 -1.94,-0.21 -2.84,-0.61l-1.49,1.49C10.02,21.62 11.51,22 13,22c2.3,0 4.61,-0.88 6.36,-2.64 3.52,-3.51 3.52,-9.21 0,-12.72z"/>
+</vector>

+ 25 - 0
app/src/main/res/drawable/rounded_rect.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Nextcloud Android client application
+
+ @author ZetaTom
+ Copyright (C) 2023 ZetaTom
+ Copyright (C) 2023 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/>.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="@color/grey_900" />
+    <corners android:radius="32dp" />
+</shape>

+ 106 - 0
app/src/main/res/layout/activity_edit_image.xml

@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Nextcloud Android client application
+
+ @author ZetaTom
+ Copyright (C) 2023 ZetaTom
+ Copyright (C) 2023 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/>.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/black">
+
+    <androidx.appcompat.widget.Toolbar
+        android:id="@+id/toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="@color/black"
+        android:elevation="4dp"
+        android:minHeight="?attr/actionBarSize"
+        android:theme="@style/Theme.ownCloud.Toolbar.AppWidgetContainer"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <com.canhub.cropper.CropImageView
+        android:id="@+id/cropImageView"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_margin="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/toolbar" />
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="24dp"
+        android:background="@drawable/rounded_rect"
+        android:elevation="4dp"
+        android:orientation="horizontal"
+        android:paddingLeft="8dp"
+        android:paddingRight="8dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent">
+
+        <ImageButton
+            android:id="@+id/rotate_left"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:layout_weight="1"
+            android:background="?android:selectableItemBackgroundBorderless"
+            android:paddingLeft="8dp"
+            android:paddingRight="8dp"
+            app:srcCompat="@drawable/outline_rotate_90_degrees_ccw_24"
+            app:tint="@color/white" />
+
+        <ImageButton
+            android:id="@+id/rotate_right"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:layout_weight="1"
+            android:background="?android:selectableItemBackgroundBorderless"
+            android:paddingLeft="8dp"
+            android:paddingRight="8dp"
+            android:rotationY="-180"
+            app:srcCompat="@drawable/outline_rotate_90_degrees_ccw_24"
+            app:tint="@color/white" />
+
+        <ImageButton
+            android:id="@+id/flip_vertical"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:layout_weight="1"
+            android:background="?android:selectableItemBackgroundBorderless"
+            android:paddingLeft="8dp"
+            android:paddingRight="8dp"
+            android:rotation="90"
+            app:srcCompat="@drawable/outline_flip_24"
+            app:tint="@color/white" />
+
+        <ImageButton
+            android:id="@+id/flip_horizontal"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:layout_weight="1"
+            android:background="?android:selectableItemBackgroundBorderless"
+            android:paddingLeft="8dp"
+            android:paddingRight="8dp"
+            app:srcCompat="@drawable/outline_flip_24"
+            app:tint="@color/white" />
+    </LinearLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 0 - 7
app/src/main/res/layout/preview_image_activity.xml

@@ -34,11 +34,4 @@
         android:layout_height="match_parent"
         android:layout_height="match_parent"
         android:layout_gravity="start"/>
         android:layout_gravity="start"/>
 
 
-    <!-- LinearLayout
-        android:id="@+id/fragment"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent" >
-            <!- - Preview: layout=@layout/preview_image_fragment - ->
-    </LinearLayout -->
-
 </androidx.drawerlayout.widget.DrawerLayout>
 </androidx.drawerlayout.widget.DrawerLayout>