Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1562,6 +1563,7 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor
}
}
}
EditorCameraHelper.handlePermissionResult(requestCode, allGranted, ::launchCamera)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good Addition: This correctly handles the permission result for Aztec editor camera launches.

Minor Observation: The return value from handlePermissionResult is not used here. This is fine since you're calling it for side effects, but consider whether other permission results in this method should also early-return if handled:

if (EditorCameraHelper.handlePermissionResult(requestCode, allGranted, ::launchCamera)) {
    return  // Early return if this helper handled it
}

This would prevent potential conflicts if multiple handlers respond to the same request code.

}

private fun handleBackPressed(): Boolean {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -2681,6 +2675,10 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor
)
}

override fun checkCameraPermissionAndLaunch() {
EditorCameraHelper.checkCameraPermissionAndLaunch(this, ::launchCamera)
}

private fun setPostContentFromShareAction() {
val intent: Intent = intent

Expand Down Expand Up @@ -3221,7 +3219,7 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor

override fun onCapturePhotoClicked() {
if (WPMediaUtils.currentUserCanUploadMedia(siteModel)) {
launchCamera()
checkCameraPermissionAndLaunch()
} else {
editorPhotoPicker?.showNoUploadPermissionSnackbar()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1497,6 +1498,7 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene
}
}
}
EditorCameraHelper.handlePermissionResult(requestCode, allGranted, ::launchCamera)
}

private fun handleBackPressed(): Boolean {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -2412,6 +2406,10 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene
)
}

override fun checkCameraPermissionAndLaunch() {
EditorCameraHelper.checkCameraPermissionAndLaunch(this, ::launchCamera)
}

private fun setPostContentFromShareAction() {
val intent: Intent = intent

Expand Down Expand Up @@ -2792,7 +2790,7 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene

override fun onCapturePhotoClicked() {
if (WPMediaUtils.currentUserCanUploadMedia(siteModel)) {
launchCamera()
checkCameraPermissionAndLaunch()
} else {
editorPhotoPicker?.showNoUploadPermissionSnackbar()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
Comment on lines +20 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Issue: This is likely the source of the crash reported by @dcalhoun.

PermissionUtils.checkAndRequestCameraAndStoragePermissions appears to expect a Fragment as the first parameter (based on usage in GutenbergEditorFragment.java:829), but here we're passing an Activity.

Suggested fix:

fun checkCameraPermissionAndLaunch(activity: Activity, launchCamera: () -> Unit) {
    // Use ActivityCompat which properly handles Activity permissions
    if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) 
        == PackageManager.PERMISSION_GRANTED) {
        launchCamera()
    } else {
        ActivityCompat.requestPermissions(
            activity,
            arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE),
            WPPermissionUtils.AZTEC_EDITOR_CAMERA_PERMISSION_REQUEST_CODE
        )
    }
}

Or investigate if PermissionUtils has an overload that accepts Activity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a false warning. PermissionUtils has a method that accepts an activity and a method that accepts a fragment.

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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

/**
Expand Down Expand Up @@ -186,11 +193,31 @@ class EditorPhotoPicker(
}
}

@Deprecated("Used only by AztecEditorFragment")
override fun onMediaToolbarButtonClicked(action: MediaToolbarAction?) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good Fix: Restoring this method implementation correctly fixes the Aztec editor regression. The previous version (from PR #22427) made this a no-op with @Deprecated, which broke the Aztec editor's media toolbar.

The implementation properly handles all three media toolbar actions:

  • GALLERY: Shows the embedded photo picker
  • CAMERA: Launches camera with permission checks
  • LIBRARY: Opens WP Media Library

Note: Ensure this is only used by Aztec. The Gutenberg editors handle media through MediaPickerListener.onIconClicked() instead.

// 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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Duplication: This permission check pattern is repeated on lines 205, 255, and 262.

Suggestion: Extract to a private helper method:

private fun canUploadMedia(): Boolean = 
    WPMediaUtils.currentUserCanUploadMedia(siteModelProvider())

Then use it as:

if (canUploadMedia()) {
    editorMediaActions.checkCameraPermissionAndLaunch()
} else {
    showNoUploadPermissionSnackbar()
}

This improves maintainability and reduces the chance of inconsistencies.

} else {
showNoUploadPermissionSnackbar()
}
}
MediaToolbarAction.LIBRARY -> {
// Open the WP Media Library
mediaPickerLauncher.viewWPMediaLibraryPickerForResult(
activity,
siteModel,
MediaBrowserType.EDITOR_PICKER
)
}
null -> { /* no-op */ }
}
}

fun onOrientationChanged(@Orientation newOrientation: Int) {
Expand Down Expand Up @@ -226,7 +253,7 @@ class EditorPhotoPicker(
when (action) {
is MediaPickerAction.OpenCameraForPhotos -> {
if (WPMediaUtils.currentUserCanUploadMedia(siteModel)) {
editorMediaActions.launchCamera()
editorMediaActions.checkCameraPermissionAndLaunch()
} else {
showNoUploadPermissionSnackbar()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good Addition: New permission request code for Aztec editor camera functionality.

Documentation Suggestion: Add a comment explaining when to use AZTEC_EDITOR_CAMERA_PERMISSION_REQUEST_CODE vs PHOTO_PICKER_CAMERA_PERMISSION_REQUEST_CODE vs EDITOR_MEDIA_PERMISSION_REQUEST_CODE:

// Used when capturing photos from Aztec editor's media toolbar
public static final int AZTEC_EDITOR_CAMERA_PERMISSION_REQUEST_CODE = 42;

This helps developers understand which constant to use in different contexts.

public static final int EDITOR_MEDIA_PERMISSION_REQUEST_CODE = 60;
public static final int READER_FILE_DOWNLOAD_PERMISSION_REQUEST_CODE = 80;

Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Loading