Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
872880b
build: bump GutenbergKit to PR 357 media-upload-delegate snapshot
dcalhoun Jul 23, 2026
663c101
feat: expose media optimization prefs and EXIF strip via wrappers
dcalhoun Jul 23, 2026
b07bf23
feat: add GBKMediaUploadProcessor for GutenbergKit media uploads
dcalhoun Jul 23, 2026
adca47a
test: cover GBKMediaUploadProcessor decision table
dcalhoun Jul 23, 2026
efce127
feat: wire GBKMediaUploadProcessor into the GutenbergKit editor
dcalhoun Jul 23, 2026
887c544
build: bump WordPress-Utils to the orientation-fix snapshot
dcalhoun Jul 23, 2026
b7ad60e
style: suppress ReturnCount on the processor's decision-table methods
dcalhoun Jul 24, 2026
bafe277
fix: strip location only from ExifInterface-writable image formats
dcalhoun Jul 24, 2026
149d909
style: drop stale SwallowedException suppression on transcodeVideo
dcalhoun Jul 24, 2026
b486aa7
docs: clarify the processFile plan check is a cache-aware fallback
dcalhoun Jul 24, 2026
f649477
build: bump GutenbergKit to the trunk media-upload snapshot
dcalhoun Aug 17, 2026
5e3ad0f
fix: set the media upload delegate before the editor loads
dcalhoun Aug 17, 2026
2a9c583
perf: skip the upload temp copy for files we pass through
dcalhoun Aug 17, 2026
d6db8eb
test: cover the handlesFile decision table
dcalhoun Aug 17, 2026
43334af
fix: normalize the upload mime type before the plan check
dcalhoun Aug 17, 2026
2480d43
fix: fall back to the original video when the transcode cannot start
dcalhoun Aug 17, 2026
a3aa9ea
fix: label a processed image from its encoded output, not its declare…
dcalhoun Aug 17, 2026
bce749e
fix: key the video duration limit off the resolved mime type
dcalhoun Aug 18, 2026
fb5f492
build: pin GutenbergKit to the v0.20.0-alpha.0 release
dcalhoun Aug 18, 2026
383be17
fix: scope the upload plan check to free WordPress.com plans
dcalhoun Aug 19, 2026
7f3da1f
perf: claim uploads only when processing would read the file
dcalhoun Aug 19, 2026
78b0577
fix: warn when the media upload delegate arrives after the view
dcalhoun Aug 19, 2026
b10438d
feat: track video optimization analytics for GutenbergKit transcodes
dcalhoun Aug 19, 2026
3e64468
fix: delete the strip-location temp file when the copy fails
dcalhoun Aug 19, 2026
08602b5
fix: build media URIs from files instead of parsing them as strings
dcalhoun Aug 19, 2026
5294b19
build: pin wordpress-utils to the PR build with the path decode fix
dcalhoun Aug 19, 2026
3ab55ca
fix: satisfy detekt in GBKMediaUploadProcessor
dcalhoun Aug 19, 2026
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 @@ -65,6 +65,7 @@ import org.wordpress.android.editor.EditorImagePreviewListener
import org.wordpress.android.editor.EditorImageSettingsListener
import org.wordpress.android.editor.ExceptionLogger
import org.wordpress.android.editor.gutenberg.DialogVisibility
import org.wordpress.android.ui.posts.editor.GBKMediaUploadProcessor
import org.wordpress.android.ui.posts.editor.GutenbergKitEditorFragment
import org.wordpress.android.ui.posts.editor.GutenbergKitNetworkLogger
import org.wordpress.android.editor.savedinstance.SavedInstanceDatabase
Expand Down Expand Up @@ -172,6 +173,7 @@ import org.wordpress.android.ui.posts.reactnative.ReactNativeRequestHandler
import org.wordpress.android.ui.posts.sharemessage.EditJetpackSocialShareMessageActivity
import org.wordpress.android.ui.posts.sharemessage.EditJetpackSocialShareMessageActivity.Companion.createIntent
import org.wordpress.android.ui.prefs.AppPrefs
import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.ui.prefs.SiteSettingsInterface
import org.wordpress.android.ui.prefs.SiteSettingsInterface.SiteSettingsListener
import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper
Expand All @@ -193,6 +195,8 @@ import org.wordpress.android.util.DateTimeUtilsWrapper
import org.wordpress.android.util.DisplayUtils
import org.wordpress.android.util.FluxCUtils
import org.wordpress.android.util.MediaUtils
import org.wordpress.android.util.MediaUtilsWrapper
import org.wordpress.android.util.SiteUtilsWrapper
import org.wordpress.android.util.NetworkUtils
import org.wordpress.android.util.ReblogUtils
import org.wordpress.android.util.ShortcutUtils
Expand All @@ -205,6 +209,7 @@ import org.wordpress.android.util.WPMediaUtils
import org.wordpress.android.util.WPPermissionUtils
import org.wordpress.android.util.WPUrlUtils
import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
import org.wordpress.android.util.analytics.AnalyticsUtilsWrapper
import org.wordpress.android.util.analytics.AnalyticsUtils
import org.wordpress.android.util.analytics.AnalyticsUtils.BlockEditorEnabledSource
import org.wordpress.android.util.config.ContactSupportFeatureConfig
Expand Down Expand Up @@ -357,6 +362,7 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene
@Inject lateinit var reblogUtils: ReblogUtils

@Inject lateinit var analyticsTrackerWrapper: AnalyticsTrackerWrapper
@Inject lateinit var analyticsUtilsWrapper: AnalyticsUtilsWrapper

@Inject lateinit var publishPostImmediatelyUseCase: PublishPostImmediatelyUseCase

Expand Down Expand Up @@ -387,6 +393,9 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene
@Inject lateinit var editorJetpackSocialViewModel: EditorJetpackSocialViewModel
@Inject lateinit var gutenbergKitNetworkLogger: GutenbergKitNetworkLogger
@Inject lateinit var gutenbergKitSettingsBuilder: GutenbergKitSettingsBuilder
@Inject lateinit var mediaUtilsWrapper: MediaUtilsWrapper
@Inject lateinit var siteUtilsWrapper: SiteUtilsWrapper
@Inject lateinit var appPrefsWrapper: AppPrefsWrapper
private lateinit var editPostNavigationViewModel: EditPostNavigationViewModel
private lateinit var editPostSettingsViewModel: EditPostSettingsViewModel
private lateinit var prepublishingViewModel: PrepublishingViewModel
Expand Down Expand Up @@ -2260,6 +2269,19 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene
}
)
}

// Process device media per the app's media settings before upload
editorFragment?.setMediaUploadDelegate(
GBKMediaUploadProcessor(
site = siteModel,
appContext = applicationContext,
mediaUtilsWrapper = mediaUtilsWrapper,
appPrefsWrapper = appPrefsWrapper,
siteUtilsWrapper = siteUtilsWrapper,
analyticsTrackerWrapper = analyticsTrackerWrapper,
analyticsUtilsWrapper = analyticsUtilsWrapper,
)
)
}
VIEW_PAGER_PAGE_SETTINGS -> editPostSettingsFragment = fragment as EditPostSettingsFragment
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import org.wordpress.gutenberg.GutenbergView.LogJsExceptionListener
import org.wordpress.gutenberg.GutenbergView.OpenMediaLibraryListener
import org.wordpress.gutenberg.GutenbergView.TitleAndContentCallback
import org.wordpress.gutenberg.Media
import org.wordpress.gutenberg.MediaUploadDelegate
import org.wordpress.gutenberg.model.EditorConfiguration
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
Expand All @@ -61,6 +62,7 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() {
private var onLogJsExceptionListener: LogJsExceptionListener? = null
private var modalDialogStateListener: GutenbergView.ModalDialogStateListener? = null
private var networkRequestListener: GutenbergView.NetworkRequestListener? = null
private var mediaUploadDelegate: MediaUploadDelegate? = null
private var rootView: View? = null
private var isXPostsEnabled: Boolean = false

Expand Down Expand Up @@ -190,6 +192,14 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() {
context = requireContext()
)

// Must be set before the editor loads: GutenbergKit captures the delegate when the page
// begins loading and throws from the setter afterward. The constructor already kicks off
// the load when dependencies are preloaded, so assign it here rather than alongside the
// listeners below.
mediaUploadDelegate?.let {
gutenbergView.mediaUploadDelegate = it
}

gutenbergViewContainer.addView(
gutenbergView,
FrameLayout.LayoutParams(
Expand Down Expand Up @@ -552,6 +562,29 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() {
gutenbergView?.setNetworkRequestListener(listener)
}

/**
* Sets the delegate that processes media before upload. Must be called before [onCreateView],
* which is the only place the delegate reaches the view: GutenbergKit captures it when the
* editor page begins loading and throws from its setter afterward, so pushing it into a live
* view here would crash. The field is the single source of truth.
*
* Unlike the other hooks here ([setNetworkRequestListener], [setImageLoader]), this one cannot
* push into an already-created view, so arriving late is a silent no-op: uploads fall back to
* GutenbergKit's unprocessed WebView path with no error. That can only happen if the view was
* created before the delegate was assigned — e.g. a configuration change restoring the fragment
* ahead of a deferred setupViewPager() callback. Log it rather than let it pass unnoticed.
*/
fun setMediaUploadDelegate(delegate: MediaUploadDelegate) {
if (gutenbergView != null) {
AppLog.w(
AppLog.T.MEDIA,
"GutenbergKitEditorFragment: media upload delegate set after the view was created" +
" - uploads will bypass the app's media settings for this session"
)
}
mediaUploadDelegate = delegate
}

override fun onUndoPressed() {
gutenbergView?.undo()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ class AppPrefsWrapper @Inject constructor(val buildConfigWrapper: BuildConfigWra
get() = AppPrefs.isAztecEditorEnabled()
set(enabled) = AppPrefs.setAztecEditorEnabled(enabled)

val isImageOptimize: Boolean
get() = AppPrefs.isImageOptimize()

val isVideoOptimize: Boolean
get() = AppPrefs.isVideoOptimize()

val videoOptimizeWidth: Int
get() = AppPrefs.getVideoOptimizeWidth()

val videoOptimizeQuality: Int
get() = AppPrefs.getVideoOptimizeQuality()

val isStripImageLocation: Boolean
get() = AppPrefs.isStripImageLocation()

var postListAuthorSelection: AuthorFilterSelection
get() = AppPrefs.getAuthorFilterSelection()
set(value) = AppPrefs.setAuthorFilterSelection(value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.wordpress.android.editor.EditorMediaUtils
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.utils.MimeTypes.Plan
import org.wordpress.android.util.AppLog.T
import java.io.File
import java.util.concurrent.TimeUnit
import javax.inject.Inject

Expand Down Expand Up @@ -35,6 +36,15 @@ class MediaUtilsWrapper @Inject constructor(private val appContext: Context) {
fun isVideoMimeType(mimeType: String?): Boolean =
org.wordpress.android.fluxc.utils.MediaUtils.isVideoMimeType(mimeType)

fun isAudioMimeType(mimeType: String?): Boolean =
org.wordpress.android.fluxc.utils.MediaUtils.isAudioMimeType(mimeType)

fun isApplicationMimeType(mimeType: String?): Boolean =
org.wordpress.android.fluxc.utils.MediaUtils.isApplicationMimeType(mimeType)

fun stripImageLocation(imagePath: String) =
org.wordpress.android.fluxc.utils.MediaUtils.stripLocation(imagePath)

fun isInMediaStore(mediaUri: Uri?): Boolean =
MediaUtils.isInMediaStore(mediaUri)

Expand All @@ -60,8 +70,28 @@ class MediaUtilsWrapper @Inject constructor(private val appContext: Context) {
fun isVideoFile(mediaUri: Uri): Boolean =
isVideo(mediaUri) || isVideoMimeType(getMimeType(mediaUri))

fun isProhibitedVideoDuration(context: Context, site: SiteModel, uri: Uri): Boolean {
if (isVideoFile(uri) && site.hasFreePlan && !site.isActiveModuleEnabled("videopress")) {
/**
* Duration check for a file whose type is already known from its upload metadata.
*
* Callers must pass the mime type they resolved, because the [Uri] overload cannot recover it
* here: a `file://` URI makes [getMimeType] return null (ContentResolver only types content
* URIs), collapsing [isVideoFile] to [MediaUtils.isVideo] — an extension-only test. A video
* whose filename carries no recognized extension would then skip the check entirely and let a
* free site upload a video over the limit.
*/
fun isProhibitedVideoDuration(context: Context, site: SiteModel, file: File, mimeType: String): Boolean =
isProhibitedVideoDuration(context, site, Uri.fromFile(file), isVideoMimeType(mimeType))

fun isProhibitedVideoDuration(context: Context, site: SiteModel, uri: Uri): Boolean =
isProhibitedVideoDuration(context, site, uri, isVideoFile(uri))

private fun isProhibitedVideoDuration(
context: Context,
site: SiteModel,
uri: Uri,
isVideo: Boolean
): Boolean {
if (isVideo && site.hasFreePlan && !site.isActiveModuleEnabled("videopress")) {
val retriever = MediaMetadataRetriever()

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ public static Uri getOptimizedMedia(Context context, String path, boolean isVide
ExifUtils.writeExifData(exifData, optimizedPath);

AnalyticsTracker.track(AnalyticsTracker.Stat.MEDIA_PHOTO_OPTIMIZED);
return Uri.parse(optimizedPath);
// fromFile, not parse: these are filesystem paths, and Uri.parse reads everything
// after a '#' as a fragment and after a '?' as a query, so a filename containing
// either (e.g. "IMG_#1.jpg") yields a Uri whose getPath() is truncated to a file that
// does not exist. fromFile encodes the path instead of parsing it.
return Uri.fromFile(new File(optimizedPath));
}
return null;
}
Expand All @@ -104,7 +108,9 @@ public static Uri fixOrientationIssue(Context context, String path, boolean isVi

String rotatedPath = ImageUtils.rotateImageIfNecessary(context, path);
if (rotatedPath != null) {
return Uri.parse(rotatedPath);
// See getOptimizedMedia above: fromFile encodes a filesystem path, Uri.parse would
// truncate it at a '#' or '?' in the filename.
return Uri.fromFile(new File(rotatedPath));
}

return null;
Expand Down
Loading
Loading