package app.fedilab.android.imageeditor; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static app.fedilab.android.imageeditor.FileSaveHelper.isSdkHigherThan28; import android.Manifest; import android.annotation.SuppressLint; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import android.provider.MediaStore; import android.view.MotionEvent; import android.view.View; import android.view.animation.AnticipateOvershootInterpolator; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.exifinterface.media.ExifInterface; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.transition.ChangeBounds; import androidx.transition.TransitionManager; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.theartofdev.edmodo.cropper.CropImage; import java.io.File; import java.io.IOException; import java.io.InputStream; import app.fedilab.android.R; import app.fedilab.android.imageeditor.base.BaseActivity; import app.fedilab.android.imageeditor.filters.FilterListener; import app.fedilab.android.imageeditor.filters.FilterViewAdapter; import app.fedilab.android.imageeditor.tools.EditingToolsAdapter; import app.fedilab.android.imageeditor.tools.ToolType; import es.dmoral.toasty.Toasty; import ja.burhanrashid52.photoeditor.OnPhotoEditorListener; import ja.burhanrashid52.photoeditor.PhotoEditor; import ja.burhanrashid52.photoeditor.PhotoEditorView; import ja.burhanrashid52.photoeditor.PhotoFilter; import ja.burhanrashid52.photoeditor.SaveSettings; import ja.burhanrashid52.photoeditor.TextStyleBuilder; import ja.burhanrashid52.photoeditor.ViewType; import ja.burhanrashid52.photoeditor.shape.ShapeBuilder; import ja.burhanrashid52.photoeditor.shape.ShapeType; public class EditImageActivity extends BaseActivity implements OnPhotoEditorListener, View.OnClickListener, PropertiesBSFragment.Properties, ShapeBSFragment.Properties, EmojiBSFragment.EmojiListener, StickerBSFragment.StickerListener, EditingToolsAdapter.OnItemSelected, FilterListener { public static final String FILE_PROVIDER_AUTHORITY = "com.burhanrashid52.photoeditor.fileprovider"; public static final String ACTION_NEXTGEN_EDIT = "action_nextgen_edit"; public static final String PINCH_TEXT_SCALABLE_INTENT_KEY = "PINCH_TEXT_SCALABLE"; private static final int CAMERA_REQUEST = 52; private static final int PICK_REQUEST = 53; private final EditingToolsAdapter mEditingToolsAdapter = new EditingToolsAdapter(this); private final FilterViewAdapter mFilterViewAdapter = new FilterViewAdapter(this); private final ConstraintSet mConstraintSet = new ConstraintSet(); PhotoEditor mPhotoEditor; @Nullable @VisibleForTesting Uri mSaveImageUri; private PhotoEditorView mPhotoEditorView; private PropertiesBSFragment mPropertiesBSFragment; private ShapeBSFragment mShapeBSFragment; private ShapeBuilder mShapeBuilder; private EmojiBSFragment mEmojiBSFragment; private StickerBSFragment mStickerBSFragment; private TextView mTxtCurrentTool; private Typeface mWonderFont; private RecyclerView mRvTools, mRvFilters; private ConstraintLayout mRootView; private boolean mIsFilterVisible; private Uri uri; private boolean exit; private FileSaveHelper mSaveFileHelper; private static int exifToDegrees(int exifOrientation) { if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) { return 90; } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_180) { return 180; } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) { return 270; } return 0; } @Override protected void onCreate(Bundle savedInstanceState) { makeFullScreen(); super.onCreate(savedInstanceState); setContentView(R.layout.activity_edit_image); Bundle b = getIntent().getExtras(); if (getSupportActionBar() != null) getSupportActionBar().hide(); String path = null; if (b != null) path = b.getString("imageUri", null); if (path == null) { finish(); } uri = Uri.parse(path); exit = false; initViews(); handleIntentImage(mPhotoEditorView.getSource()); mWonderFont = Typeface.createFromAsset(getAssets(), "beyond_wonderland.ttf"); mPropertiesBSFragment = new PropertiesBSFragment(); mEmojiBSFragment = new EmojiBSFragment(); mStickerBSFragment = new StickerBSFragment(); mShapeBSFragment = new ShapeBSFragment(); mStickerBSFragment.setStickerListener(this); mEmojiBSFragment.setEmojiListener(this); mPropertiesBSFragment.setPropertiesChangeListener(this); mShapeBSFragment.setPropertiesChangeListener(this); LinearLayoutManager llmTools = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false); mRvTools.setLayoutManager(llmTools); mRvTools.setAdapter(mEditingToolsAdapter); LinearLayoutManager llmFilters = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false); mRvFilters.setLayoutManager(llmFilters); mRvFilters.setAdapter(mFilterViewAdapter); // NOTE(lucianocheng): Used to set integration testing parameters to PhotoEditor boolean pinchTextScalable = getIntent().getBooleanExtra(PINCH_TEXT_SCALABLE_INTENT_KEY, true); //Typeface mTextRobotoTf = ResourcesCompat.getFont(this, R.font.roboto_medium); //Typeface mEmojiTypeFace = Typeface.createFromAsset(getAssets(), "emojione-android.ttf"); Typeface mEmojiTypeFace = Typeface.createFromAsset(getAssets(), "emojione-android.ttf"); mPhotoEditor = new PhotoEditor.Builder(this, mPhotoEditorView) .setPinchTextScalable(pinchTextScalable) // set flag to make text scalable when pinch //.setDefaultTextTypeface(mTextRobotoTf) .setPinchTextScalable(true) .setDefaultEmojiTypeface(mEmojiTypeFace) .build(); // build photo editor sdk mPhotoEditor.setOnPhotoEditorListener(this); //Set Image Dynamically try { mPhotoEditorView.getSource().setImageURI(uri); } catch (Exception e) { Toasty.error(EditImageActivity.this, getString(R.string.toast_error)).show(); } if (uri != null) { try (InputStream inputStream = getContentResolver().openInputStream(uri)) { assert inputStream != null; ExifInterface exif = new ExifInterface(inputStream); int rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); int rotationInDegrees = exifToDegrees(rotation); mPhotoEditorView.getSource().setRotation(rotationInDegrees); } catch (Exception e) { e.printStackTrace(); } } Button send = findViewById(R.id.send); send.setOnClickListener(v -> { exit = true; saveImage(); }); } private void handleIntentImage(ImageView source) { Intent intent = getIntent(); if (intent != null) { // NOTE(lucianocheng): Using "yoda conditions" here to guard against // a null Action in the Intent. if (Intent.ACTION_EDIT.equals(intent.getAction()) || ACTION_NEXTGEN_EDIT.equals(intent.getAction())) { try { Uri uri = intent.getData(); Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri); source.setImageBitmap(bitmap); } catch (IOException e) { e.printStackTrace(); } } else { String intentType = intent.getType(); if (intentType != null && intentType.startsWith("image/")) { Uri imageUri = intent.getData(); if (imageUri != null) { source.setImageURI(imageUri); } } } } } private void initViews() { ImageView imgUndo; ImageView imgRedo; ImageView imgCamera; ImageView imgGallery; ImageView imgSave; ImageView imgClose; ImageView imgCrop; mPhotoEditorView = findViewById(R.id.photoEditorView); mTxtCurrentTool = findViewById(R.id.txtCurrentTool); mRvTools = findViewById(R.id.rvConstraintTools); mRvFilters = findViewById(R.id.rvFilterView); mRootView = findViewById(R.id.rootView); imgUndo = findViewById(R.id.imgUndo); imgUndo.setOnClickListener(this); imgRedo = findViewById(R.id.imgRedo); imgRedo.setOnClickListener(this); imgCamera = findViewById(R.id.imgCamera); imgCamera.setOnClickListener(this); imgGallery = findViewById(R.id.imgGallery); imgGallery.setOnClickListener(this); imgSave = findViewById(R.id.imgSave); imgSave.setOnClickListener(this); imgClose = findViewById(R.id.imgClose); imgClose.setOnClickListener(this); imgCrop = findViewById(R.id.imgCrop); imgCrop.setOnClickListener(this); } @Override public void onEditTextChangeListener(final View rootView, String text, int colorCode) { TextEditorDialogFragment textEditorDialogFragment = TextEditorDialogFragment.show(this, text, colorCode); textEditorDialogFragment.setOnTextEditorListener((inputText, newColorCode) -> { final TextStyleBuilder styleBuilder = new TextStyleBuilder(); styleBuilder.withTextColor(newColorCode); mPhotoEditor.editText(rootView, inputText, styleBuilder); mTxtCurrentTool.setText(R.string.label_text); }); } @Override public void onAddViewListener(ViewType viewType, int numberOfAddedViews) { } @Override public void onRemoveViewListener(ViewType viewType, int numberOfAddedViews) { } @Override public void onStartViewChangeListener(ViewType viewType) { } @Override public void onStopViewChangeListener(ViewType viewType) { } @Override public void onTouchSourceImage(MotionEvent event) { } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()) { case R.id.imgUndo: mPhotoEditor.undo(); break; case R.id.imgRedo: mPhotoEditor.redo(); break; case R.id.imgSave: saveImage(); break; case R.id.imgClose: onBackPressed(); break; case R.id.imgCrop: shareImage(); break; case R.id.imgCamera: Intent cameraIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); startActivityForResult(cameraIntent, CAMERA_REQUEST); break; case R.id.imgGallery: Intent intent = new Intent(); intent.setType("image/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, "Select Picture"), PICK_REQUEST); break; } } private void shareImage() { if (mSaveImageUri == null) { //showSnackbar(getString(R.string.msg_save_image_to_share)); return; } Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("image/*"); intent.putExtra(Intent.EXTRA_STREAM, buildFileProviderUri(mSaveImageUri)); startActivity(Intent.createChooser(intent, getString(R.string.msg_share_image))); } private Uri buildFileProviderUri(@NonNull Uri uri) { return FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, new File(uri.getPath())); } private void saveImage() { final String fileName = System.currentTimeMillis() + ".png"; final boolean hasStoragePermission = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED; if (hasStoragePermission || isSdkHigherThan28()) { showLoading("Saving..."); mSaveFileHelper.createFile(fileName, (fileCreated, filePath, error, uri) -> { if (fileCreated) { SaveSettings saveSettings = new SaveSettings.Builder() .setClearViewsEnabled(true) .setTransparencyEnabled(true) .build(); mPhotoEditor.saveAsFile(filePath, saveSettings, new PhotoEditor.OnSaveListener() { @Override public void onSuccess(@NonNull String imagePath) { mSaveFileHelper.notifyThatFileIsNowPubliclyAvailable(getContentResolver()); hideLoading(); showSnackbar("Image Saved Successfully"); mSaveImageUri = uri; mPhotoEditorView.getSource().setImageURI(mSaveImageUri); } @Override public void onFailure(@NonNull Exception exception) { hideLoading(); showSnackbar("Failed to save Image"); } }); } else { hideLoading(); showSnackbar(error); } }); } else { requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { ExifInterface exif; int rotation; int rotationInDegrees = 0; if (data != null && data.getData() != null) { try (InputStream inputStream = getContentResolver().openInputStream(data.getData())) { assert inputStream != null; exif = new ExifInterface(inputStream); rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); rotationInDegrees = exifToDegrees(rotation); } catch (Exception e) { e.printStackTrace(); } } switch (requestCode) { case CAMERA_REQUEST: if (data != null && data.getExtras() != null) { mPhotoEditor.clearAllViews(); Bitmap photo = (Bitmap) data.getExtras().get("data"); mPhotoEditorView.getSource().setImageBitmap(photo); mPhotoEditorView.getSource().setRotation(rotationInDegrees); } break; case PICK_REQUEST: if (data != null && data.getData() != null) { try { mPhotoEditor.clearAllViews(); Uri uri = data.getData(); Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri); mPhotoEditorView.getSource().setImageBitmap(bitmap); mPhotoEditorView.getSource().setRotation(rotationInDegrees); } catch (IOException e) { e.printStackTrace(); } } break; case CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE: CropImage.ActivityResult result = CropImage.getActivityResult(data); if (result != null) { Uri resultUri = result.getUri(); if (resultUri != null) { mPhotoEditorView.getSource().setImageURI(resultUri); mPhotoEditorView.getSource().setRotation(rotationInDegrees); if (uri != null && uri.getPath() != null) { File fdelete = new File(uri.getPath()); if (fdelete.exists()) { //noinspection ResultOfMethodCallIgnored fdelete.delete(); } } uri = resultUri; } } break; } } } @Override public void onColorChanged(int colorCode) { mPhotoEditor.setShape(mShapeBuilder.withShapeColor(colorCode)); mTxtCurrentTool.setText(R.string.label_brush); } @Override public void onOpacityChanged(int opacity) { mPhotoEditor.setShape(mShapeBuilder.withShapeOpacity(opacity)); mTxtCurrentTool.setText(R.string.label_brush); } @Override public void onShapeSizeChanged(int shapeSize) { mPhotoEditor.setShape(mShapeBuilder.withShapeSize(shapeSize)); mTxtCurrentTool.setText(R.string.label_brush); } @Override public void onShapePicked(ShapeType shapeType) { mPhotoEditor.setShape(mShapeBuilder.withShapeType(shapeType)); } @Override public void onEmojiClick(String emojiUnicode) { mPhotoEditor.addEmoji(emojiUnicode); mTxtCurrentTool.setText(R.string.label_emoji); } @Override public void onStickerClick(Bitmap bitmap) { mPhotoEditor.addImage(bitmap); mTxtCurrentTool.setText(R.string.label_sticker); } private void showSaveDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(getString(R.string.msg_save_image)); builder.setPositiveButton("Save", (dialog, which) -> saveImage()); builder.setNegativeButton("Cancel", (dialog, which) -> dialog.dismiss()); builder.setNeutralButton("Discard", (dialog, which) -> finish()); builder.create().show(); } @Override public void onFilterSelected(PhotoFilter photoFilter) { mPhotoEditor.setFilterEffect(photoFilter); } @Override public void onToolSelected(ToolType toolType) { switch (toolType) { case SHAPE: mPhotoEditor.setBrushDrawingMode(true); mShapeBuilder = new ShapeBuilder(); mPhotoEditor.setShape(mShapeBuilder); mTxtCurrentTool.setText(R.string.label_shape); showBottomSheetDialogFragment(mShapeBSFragment); break; case TEXT: TextEditorDialogFragment textEditorDialogFragment = TextEditorDialogFragment.show(this); textEditorDialogFragment.setOnTextEditorListener((inputText, colorCode) -> { final TextStyleBuilder styleBuilder = new TextStyleBuilder(); styleBuilder.withTextColor(colorCode); mPhotoEditor.addText(inputText, styleBuilder); mTxtCurrentTool.setText(R.string.label_text); }); break; case ERASER: mPhotoEditor.brushEraser(); mTxtCurrentTool.setText(R.string.label_eraser_mode); break; case FILTER: mTxtCurrentTool.setText(R.string.label_filter); showFilter(true); break; case EMOJI: showBottomSheetDialogFragment(mEmojiBSFragment); break; case STICKER: showBottomSheetDialogFragment(mStickerBSFragment); break; case BRUSH: mPhotoEditor.setBrushDrawingMode(true); mTxtCurrentTool.setText(R.string.label_brush); mPropertiesBSFragment.show(getSupportFragmentManager(), mPropertiesBSFragment.getTag()); break; } } private void showBottomSheetDialogFragment(BottomSheetDialogFragment fragment) { if (fragment == null || fragment.isAdded()) { return; } fragment.show(getSupportFragmentManager(), fragment.getTag()); } void showFilter(boolean isVisible) { mIsFilterVisible = isVisible; mConstraintSet.clone(mRootView); if (isVisible) { mConstraintSet.clear(mRvFilters.getId(), ConstraintSet.START); mConstraintSet.connect(mRvFilters.getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START); mConstraintSet.connect(mRvFilters.getId(), ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END); } else { mConstraintSet.connect(mRvFilters.getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.END); mConstraintSet.clear(mRvFilters.getId(), ConstraintSet.END); } ChangeBounds changeBounds = new ChangeBounds(); changeBounds.setDuration(350); changeBounds.setInterpolator(new AnticipateOvershootInterpolator(1.0f)); TransitionManager.beginDelayedTransition(mRootView, changeBounds); mConstraintSet.applyTo(mRootView); } @Override public void onBackPressed() { if (mIsFilterVisible) { showFilter(false); mTxtCurrentTool.setText(R.string.app_name); } else if (!mPhotoEditor.isCacheEmpty()) { showSaveDialog(); } else { super.onBackPressed(); } } }