diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt index b957ce5bea32..0716052ccd18 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt @@ -162,6 +162,7 @@ import org.wordpress.android.ui.posts.RemotePreviewLogicHelper.PreviewLogicOpera import org.wordpress.android.ui.posts.RemotePreviewLogicHelper.RemotePreviewHelperFunctions import org.wordpress.android.ui.posts.RemotePreviewLogicHelper.RemotePreviewType import org.wordpress.android.ui.posts.editor.EditorActionsProvider +import org.wordpress.android.ui.posts.editor.EditorCameraHelper import org.wordpress.android.ui.posts.editor.EditorMediaActions import org.wordpress.android.ui.posts.editor.EditorMenuHelper import org.wordpress.android.ui.posts.editor.EditorPhotoPicker @@ -1562,6 +1563,7 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor } } } + EditorCameraHelper.handlePermissionResult(requestCode, allGranted, ::launchCamera) } private fun handleBackPressed(): Boolean { @@ -1936,14 +1938,6 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor editorMediaUploadListener?.onMediaUploadProgress(localMediaId, progress) } - private fun launchPictureLibrary() { - WPMediaUtils.launchPictureLibrary(this, editorPhotoPicker?.allowMultipleSelection == true) - } - - private fun launchVideoLibrary() { - WPMediaUtils.launchVideoLibrary(this, editorPhotoPicker?.allowMultipleSelection == true) - } - private fun launchVideoCamera() { WPMediaUtils.launchVideoCamera(this) } @@ -2681,6 +2675,10 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor ) } + override fun checkCameraPermissionAndLaunch() { + EditorCameraHelper.checkCameraPermissionAndLaunch(this, ::launchCamera) + } + private fun setPostContentFromShareAction() { val intent: Intent = intent @@ -3221,7 +3219,7 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor override fun onCapturePhotoClicked() { if (WPMediaUtils.currentUserCanUploadMedia(siteModel)) { - launchCamera() + checkCameraPermissionAndLaunch() } else { editorPhotoPicker?.showNoUploadPermissionSnackbar() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt index 24f3377ca009..3cf73297b0dc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt @@ -146,6 +146,7 @@ import org.wordpress.android.ui.posts.RemotePreviewLogicHelper.PreviewLogicOpera import org.wordpress.android.ui.posts.RemotePreviewLogicHelper.RemotePreviewHelperFunctions import org.wordpress.android.ui.posts.RemotePreviewLogicHelper.RemotePreviewType import org.wordpress.android.ui.posts.editor.EditorActionsProvider +import org.wordpress.android.ui.posts.editor.EditorCameraHelper import org.wordpress.android.ui.posts.editor.EditorMediaActions import org.wordpress.android.ui.posts.editor.EditorMenuHelper import org.wordpress.android.ui.posts.editor.EditorPhotoPicker @@ -1497,6 +1498,7 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene } } } + EditorCameraHelper.handlePermissionResult(requestCode, allGranted, ::launchCamera) } private fun handleBackPressed(): Boolean { @@ -1827,14 +1829,6 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene } } - private fun launchPictureLibrary() { - WPMediaUtils.launchPictureLibrary(this, editorPhotoPicker?.allowMultipleSelection == true) - } - - private fun launchVideoLibrary() { - WPMediaUtils.launchVideoLibrary(this, editorPhotoPicker?.allowMultipleSelection == true) - } - private fun launchVideoCamera() { WPMediaUtils.launchVideoCamera(this) } @@ -2412,6 +2406,10 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene ) } + override fun checkCameraPermissionAndLaunch() { + EditorCameraHelper.checkCameraPermissionAndLaunch(this, ::launchCamera) + } + private fun setPostContentFromShareAction() { val intent: Intent = intent @@ -2792,7 +2790,7 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene override fun onCapturePhotoClicked() { if (WPMediaUtils.currentUserCanUploadMedia(siteModel)) { - launchCamera() + checkCameraPermissionAndLaunch() } else { editorPhotoPicker?.showNoUploadPermissionSnackbar() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/EditorCameraHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/EditorCameraHelper.kt new file mode 100644 index 000000000000..2b519cef31a1 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/EditorCameraHelper.kt @@ -0,0 +1,52 @@ +package org.wordpress.android.ui.posts.editor + +import android.app.Activity +import org.wordpress.android.util.PermissionUtils +import org.wordpress.android.util.WPPermissionUtils + +/** + * Helper object for camera-related permission handling in editor activities. + * This centralizes the camera permission logic to avoid duplication between + * EditPostActivity and GutenbergKitActivity. + */ +object EditorCameraHelper { + /** + * Checks for camera permissions and launches the camera if granted. + * If permissions are not granted, requests them from the user. + * + * @param activity The activity requesting the permission + * @param launchCamera Callback to launch the camera when permission is granted + */ + fun checkCameraPermissionAndLaunch(activity: Activity, launchCamera: () -> Unit) { + if (PermissionUtils.checkAndRequestCameraAndStoragePermissions( + activity, + WPPermissionUtils.AZTEC_EDITOR_CAMERA_PERMISSION_REQUEST_CODE + ) + ) { + launchCamera() + } + } + + /** + * Handles the camera permission result in onRequestPermissionsResult. + * Should be called when requestCode matches AZTEC_EDITOR_CAMERA_PERMISSION_REQUEST_CODE. + * + * @param requestCode The permission request code + * @param allGranted Whether all requested permissions were granted + * @param launchCamera Callback to launch the camera when permission is granted + * @return true if this helper handled the request code, false otherwise + */ + fun handlePermissionResult( + requestCode: Int, + allGranted: Boolean, + launchCamera: () -> Unit + ): Boolean { + if (requestCode == WPPermissionUtils.AZTEC_EDITOR_CAMERA_PERMISSION_REQUEST_CODE) { + if (allGranted) { + launchCamera() + } + return true + } + return false + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/EditorPhotoPicker.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/EditorPhotoPicker.kt index 36cf92d8053f..707c6691a493 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/EditorPhotoPicker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/EditorPhotoPicker.kt @@ -39,6 +39,13 @@ interface EditorPhotoPickerListener { */ interface EditorMediaActions { fun launchCamera() + + /** + * Checks for camera permissions and launches the camera if granted. + * If permissions are not granted, requests them from the user. + * The activity should handle the permission result and call launchCamera() when granted. + */ + fun checkCameraPermissionAndLaunch() } /** @@ -186,11 +193,31 @@ class EditorPhotoPicker( } } - @Deprecated("Used only by AztecEditorFragment") override fun onMediaToolbarButtonClicked(action: MediaToolbarAction?) { - // MediaPickerFragment handles its own toolbar actions through FAB and menu, - // so this method is no longer needed for the embedded picker. - // The actions are now handled through MediaPickerListener.onIconClicked() + val siteModel = siteModelProvider() + when (action) { + MediaToolbarAction.GALLERY -> { + // Show the embedded photo picker for selecting media from device + showPhotoPicker(siteModel) + } + MediaToolbarAction.CAMERA -> { + // Launch the camera to capture a photo (after checking permissions) + if (WPMediaUtils.currentUserCanUploadMedia(siteModel)) { + editorMediaActions.checkCameraPermissionAndLaunch() + } else { + showNoUploadPermissionSnackbar() + } + } + MediaToolbarAction.LIBRARY -> { + // Open the WP Media Library + mediaPickerLauncher.viewWPMediaLibraryPickerForResult( + activity, + siteModel, + MediaBrowserType.EDITOR_PICKER + ) + } + null -> { /* no-op */ } + } } fun onOrientationChanged(@Orientation newOrientation: Int) { @@ -226,7 +253,7 @@ class EditorPhotoPicker( when (action) { is MediaPickerAction.OpenCameraForPhotos -> { if (WPMediaUtils.currentUserCanUploadMedia(siteModel)) { - editorMediaActions.launchCamera() + editorMediaActions.checkCameraPermissionAndLaunch() } else { showNoUploadPermissionSnackbar() } diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPPermissionUtils.java b/WordPress/src/main/java/org/wordpress/android/util/WPPermissionUtils.java index e50ecd0a8109..45f373f3a859 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/WPPermissionUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/util/WPPermissionUtils.java @@ -35,6 +35,7 @@ public class WPPermissionUtils { public static final int MEDIA_PREVIEW_PERMISSION_REQUEST_CODE = 30; public static final int PHOTO_PICKER_MEDIA_PERMISSION_REQUEST_CODE = 40; public static final int PHOTO_PICKER_CAMERA_PERMISSION_REQUEST_CODE = 41; + public static final int AZTEC_EDITOR_CAMERA_PERMISSION_REQUEST_CODE = 42; public static final int EDITOR_MEDIA_PERMISSION_REQUEST_CODE = 60; public static final int READER_FILE_DOWNLOAD_PERMISSION_REQUEST_CODE = 80; diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/EditorCameraHelperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/EditorCameraHelperTest.kt new file mode 100644 index 000000000000..c72aab603c81 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/EditorCameraHelperTest.kt @@ -0,0 +1,107 @@ +package org.wordpress.android.ui.posts.editor + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnitRunner +import org.wordpress.android.util.WPPermissionUtils + +@RunWith(MockitoJUnitRunner::class) +class EditorCameraHelperTest { + private var cameraLaunched = false + private val launchCamera: () -> Unit = { cameraLaunched = true } + + @Test + fun `handlePermissionResult launches camera when requestCode matches and permissions granted`() { + // Arrange + cameraLaunched = false + val requestCode = WPPermissionUtils.AZTEC_EDITOR_CAMERA_PERMISSION_REQUEST_CODE + + // Act + val handled = EditorCameraHelper.handlePermissionResult( + requestCode = requestCode, + allGranted = true, + launchCamera = launchCamera + ) + + // Assert + assertThat(handled).isTrue() + assertThat(cameraLaunched).isTrue() + } + + @Test + fun `handlePermissionResult does not launch camera when requestCode matches but permissions denied`() { + // Arrange + cameraLaunched = false + val requestCode = WPPermissionUtils.AZTEC_EDITOR_CAMERA_PERMISSION_REQUEST_CODE + + // Act + val handled = EditorCameraHelper.handlePermissionResult( + requestCode = requestCode, + allGranted = false, + launchCamera = launchCamera + ) + + // Assert + assertThat(handled).isTrue() + assertThat(cameraLaunched).isFalse() + } + + @Test + fun `handlePermissionResult returns false when requestCode does not match`() { + // Arrange + cameraLaunched = false + val differentRequestCode = 999 + + // Act + val handled = EditorCameraHelper.handlePermissionResult( + requestCode = differentRequestCode, + allGranted = true, + launchCamera = launchCamera + ) + + // Assert + assertThat(handled).isFalse() + assertThat(cameraLaunched).isFalse() + } + + @Test + fun `handlePermissionResult does not launch camera when requestCode does not match even if granted`() { + // Arrange + cameraLaunched = false + val differentRequestCode = WPPermissionUtils.EDITOR_MEDIA_PERMISSION_REQUEST_CODE + + // Act + val handled = EditorCameraHelper.handlePermissionResult( + requestCode = differentRequestCode, + allGranted = true, + launchCamera = launchCamera + ) + + // Assert + assertThat(handled).isFalse() + assertThat(cameraLaunched).isFalse() + } + + @Test + fun `handlePermissionResult returns true for camera request code regardless of grant status`() { + // Arrange + val requestCode = WPPermissionUtils.AZTEC_EDITOR_CAMERA_PERMISSION_REQUEST_CODE + + // Act & Assert - granted + val handledWhenGranted = EditorCameraHelper.handlePermissionResult( + requestCode = requestCode, + allGranted = true, + launchCamera = {} + ) + assertThat(handledWhenGranted).isTrue() + + // Act & Assert - denied + val handledWhenDenied = EditorCameraHelper.handlePermissionResult( + requestCode = requestCode, + allGranted = false, + launchCamera = {} + ) + assertThat(handledWhenDenied).isTrue() + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/EditorPhotoPickerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/EditorPhotoPickerTest.kt index c0fd2721d8e2..6ef7e42408e2 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/EditorPhotoPickerTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/EditorPhotoPickerTest.kt @@ -13,6 +13,8 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.editor.MediaToolbarAction import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.ui.mediapicker.MediaItem.Identifier import org.wordpress.android.ui.mediapicker.MediaItem.Identifier.GifMediaIdentifier @@ -218,7 +220,7 @@ class EditorPhotoPickerTest { editorPhotoPicker.onIconClicked(action) // Assert - verify(editorMediaActions).launchCamera() + verify(editorMediaActions).checkCameraPermissionAndLaunch() } @Test @@ -322,12 +324,67 @@ class EditorPhotoPickerTest { // region onMediaToolbarButtonClicked tests - @Suppress("DEPRECATION") @Test - fun `onMediaToolbarButtonClicked does nothing (no-op implementation)`() { - // This method is intentionally empty as noted in the implementation - // Just verify it doesn't throw + fun `onMediaToolbarButtonClicked with null does nothing`() { + // null action is a no-op - just verify it doesn't throw editorPhotoPicker.onMediaToolbarButtonClicked(null) + + // Verify no interactions occurred + verify(editorMediaActions, never()).launchCamera() + verify(mediaPickerLauncher, never()).viewWPMediaLibraryPickerForResult( + any(), any(), any(), any() + ) + } + + @Test + fun `onMediaToolbarButtonClicked with CAMERA launches camera when user can upload`() { + // Arrange + setupSiteWithUploadPermission() + + // Act + editorPhotoPicker.onMediaToolbarButtonClicked(MediaToolbarAction.CAMERA) + + // Assert + verify(editorMediaActions).checkCameraPermissionAndLaunch() + } + + @Test + fun `onMediaToolbarButtonClicked with CAMERA does not launch camera when user cannot upload`() { + // Arrange - set up a WPCom site without upload permission + setupSiteWithoutUploadPermission() + + // Act + editorPhotoPicker.onMediaToolbarButtonClicked(MediaToolbarAction.CAMERA) + + // Assert + verify(editorMediaActions, never()).checkCameraPermissionAndLaunch() + } + + @Test + fun `onMediaToolbarButtonClicked with LIBRARY launches WP media library`() { + // Act + editorPhotoPicker.onMediaToolbarButtonClicked(MediaToolbarAction.LIBRARY) + + // Assert + verify(mediaPickerLauncher).viewWPMediaLibraryPickerForResult(any(), any(), any(), any()) + } + + @Test + fun `onMediaToolbarButtonClicked with GALLERY notifies listener that picker is shown`() { + // Arrange - set up fragment manager mock for showPhotoPicker + val fragmentManager = mock() + val fragmentTransaction = mock() + whenever(activity.supportFragmentManager).thenReturn(fragmentManager) + whenever(fragmentManager.findFragmentByTag(any())).thenReturn(null) + whenever(fragmentManager.beginTransaction()).thenReturn(fragmentTransaction) + whenever(fragmentTransaction.add(any(), any(), any())) + .thenReturn(fragmentTransaction) + + // Act + editorPhotoPicker.onMediaToolbarButtonClicked(MediaToolbarAction.GALLERY) + + // Assert + verify(editorPhotoPickerListener).onPhotoPickerShown() } // endregion @@ -339,5 +396,12 @@ class EditorPhotoPickerTest { siteModel.hasCapabilityUploadFiles = true } + private fun setupSiteWithoutUploadPermission() { + // Set up a WPCom site without upload permission + // (self-hosted sites always allow uploads, so we need a WPCom site to test no permission) + siteModel.setIsWPCom(true) + siteModel.hasCapabilityUploadFiles = false + } + // endregion }