Skip to content

Commit

Permalink
Merge pull request #11746 from nextcloud/image-editor
Browse files Browse the repository at this point in the history
Add image editor
  • Loading branch information
tobiasKaminsky authored Jul 4, 2023
2 parents bb14f82 + bc16ceb commit 0b75f90
Show file tree
Hide file tree
Showing 17 changed files with 459 additions and 36 deletions.
3 changes: 3 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ dependencies {
implementation "io.noties:prism4j:$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') {
['org.apache.commons', 'commons-logging'].each {
exclude group: "$it"
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,10 @@
android:name="com.nextcloud.client.documentscan.DocumentScanActivity"
android:exported="false"
android:theme="@style/Theme.ownCloud.Toolbar" />
<activity
android:name="com.nextcloud.client.editimage.EditImageActivity"
android:exported="false"
android:theme="@style/Theme.ownCloud.Toolbar.NullBackground" />

<activity
android:name="com.nmc.android.ui.LauncherActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package com.nextcloud.client.di;

import com.nextcloud.client.documentscan.DocumentScanActivity;
import com.nextcloud.client.editimage.EditImageActivity;
import com.nextcloud.client.etm.EtmActivity;
import com.nextcloud.client.files.downloader.FileTransferService;
import com.nextcloud.client.jobs.NotificationWork;
Expand Down Expand Up @@ -471,4 +472,7 @@ abstract class ComponentsModule {
@ContributesAndroidInjector
abstract LauncherActivity launcherActivity();

@ContributesAndroidInjector
abstract EditImageActivity editImageActivity();

}
191 changes: 191 additions & 0 deletions app/src/main/java/com/nextcloud/client/editimage/EditImageActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* 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.Build
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, getString(R.string.image_editor_unable_to_edit_image))
return
}
val resultUri = result.getUriFilePath(this, false)
val newFileName = file.fileName.substring(0, file.fileName.lastIndexOf('.')) +
" " + getString(R.string.image_editor_file_edited_suffix) +
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, getString(R.string.image_editor_unable_to_edit_image))
return
}
view.visibility = View.VISIBLE
}

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
// add save button to action bar
menuInflater.inflate(R.menu.custom_menu_placeholder, menu)
val saveIcon = AppCompatResources.getDrawable(this, R.drawable.ic_check)?.also {
DrawableCompat.setTint(it, resources.getColor(R.color.white, theme))
}
menu?.findItem(R.id.custom_menu_placeholder_item)?.apply {
icon = saveIcon
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
contentDescription = getString(R.string.common_save)
}
}
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
}
}

/**
* Set up image cropper and image editor control strip.
*/
private fun setupCropper() {
val cropper = binding.cropImageView

@Suppress("MagicNumber")
binding.rotateLeft.setOnClickListener {
cropper.rotateImage(-90)
}

@Suppress("MagicNumber")
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)

// determine output file format
format = when (file.mimeType) {
MimeType.PNG -> Bitmap.CompressFormat.PNG
MimeType.WEBP -> Bitmap.CompressFormat.WEBP
else -> Bitmap.CompressFormat.JPEG
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import com.nextcloud.android.files.FileLockingHelper;
import com.nextcloud.client.account.User;
import com.nextcloud.client.editimage.EditImageActivity;
import com.nextcloud.utils.EditorUtils;
import com.owncloud.android.R;
import com.owncloud.android.datamodel.FileDataStorageManager;
Expand Down Expand Up @@ -282,7 +283,8 @@ private void filterEdit(

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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import com.nextcloud.client.appinfo.AppInfo;
import com.nextcloud.client.core.AsyncRunner;
import com.nextcloud.client.di.Injectable;
import com.nextcloud.client.editimage.EditImageActivity;
import com.nextcloud.client.files.DeepLinkHandler;
import com.nextcloud.client.media.PlayerServiceConnection;
import com.nextcloud.client.network.ClientFactory;
Expand Down Expand Up @@ -1511,18 +1512,20 @@ public void onReceive(Context context, Intent intent) {
if (mWaitingToSend != null) {
// update file after downloading
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() &&
OCFileListFragment.DOWNLOAD_SEND.equals(downloadBehaviour)) {
String packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME);
String activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME);

sendDownloadedFile(packageName, activityName);
}
}

if (mWaitingToPreview != null) {
mWaitingToPreview = getStorageManager().getFileByRemoteId(mWaitingToPreview.getRemoteId());
if (mWaitingToPreview != null && mWaitingToPreview.isDown() &&
EditImageActivity.OPEN_IMAGE_EDITOR.equals(downloadBehaviour)) {
startImageEditor(mWaitingToPreview);
}
}
}
Expand Down Expand Up @@ -2276,6 +2279,27 @@ public void startDownloadForPreview(OCFile file, OCFile parentFolder) {
}


/**
* 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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import com.nextcloud.client.di.Injectable;
import com.nextcloud.client.documentscan.AppScanOptionalFeature;
import com.nextcloud.client.documentscan.DocumentScanActivity;
import com.nextcloud.client.editimage.EditImageActivity;
import com.nextcloud.client.jobs.BackgroundJobManager;
import com.nextcloud.client.network.ClientFactory;
import com.nextcloud.client.preferences.AppPreferences;
Expand Down Expand Up @@ -1168,6 +1169,8 @@ public boolean onFileActionChosen(@IdRes final int itemId, Set<OCFile> checkedFi
if (editorUtils.isEditorAvailable(accountManager.getUser(),
singleFile.getMimeType())) {
mContainerActivity.getFileOperationsHelper().openFileWithTextEditor(singleFile, getContext());
} else if (EditImageActivity.Companion.canBePreviewed(singleFile)) {
((FileDisplayActivity) mContainerActivity).startImageEditor(singleFile);
} else {
mContainerActivity.getFileOperationsHelper().openFileAsRichDocument(singleFile, getContext());
}
Expand Down
Loading

0 comments on commit 0b75f90

Please sign in to comment.