diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java index 6f1f81e55dfc..1c16c014490a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java @@ -953,6 +953,26 @@ public static void viewBlogAdmin(Context context, SiteModel site) { openUrlExternal(context, site.getAdminUrl()); } + public static void addNewPostWithContentFromAIForResult( + Activity activity, + SiteModel site, + boolean isPromo, + PagePostCreationSourcesDetail source, + final String content + ) { + if (site == null) { + return; + } + + Intent intent = new Intent(activity, EditPostActivity.class); + intent.putExtra(WordPress.SITE, site); + intent.putExtra(EditPostActivityConstants.EXTRA_IS_PAGE, false); + intent.putExtra(EditPostActivityConstants.EXTRA_IS_PROMO, isPromo); + intent.putExtra(AnalyticsUtils.EXTRA_CREATION_SOURCE_DETAIL, source); + intent.putExtra(EditPostActivityConstants.EXTRA_VOICE_CONTENT, content); + activity.startActivityForResult(intent, RequestCodes.EDIT_POST); + } + public static void addNewPostForResult( Activity activity, SiteModel site, 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 16db759bfb31..caf240ccdb63 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 @@ -309,6 +309,7 @@ class EditPostActivity : LocaleAwareActivity(), EditorFragmentActivity, EditorIm private var postLoadingState: PostLoadingState = PostLoadingState.NONE private var isXPostsCapable: Boolean? = null private var onGetSuggestionResult: Consumer? = null + private var isVoiceContentSet = false // For opening the context menu after permissions have been granted private var menuView: View? = null @@ -717,6 +718,7 @@ class EditPostActivity : LocaleAwareActivity(), EditorFragmentActivity, EditorIm } isNewPost = state.getBoolean(EditPostActivityConstants.STATE_KEY_IS_NEW_POST, false) + isVoiceContentSet = state.getBoolean(EditPostActivityConstants.STATE_KEY_IS_VOICE_CONTENT_SET, false) updatePostLoadingAndDialogState( fromInt( state.getInt(EditPostActivityConstants.STATE_KEY_POST_LOADING_STATE, 0) @@ -1185,6 +1187,7 @@ class EditPostActivity : LocaleAwareActivity(), EditorFragmentActivity, EditorIm } outState.putInt(EditPostActivityConstants.STATE_KEY_POST_LOADING_STATE, postLoadingState.value) outState.putBoolean(EditPostActivityConstants.STATE_KEY_IS_NEW_POST, isNewPost) + outState.putBoolean(EditPostActivityConstants.STATE_KEY_IS_VOICE_CONTENT_SET, isVoiceContentSet) outState.putBoolean( EditPostActivityConstants.STATE_KEY_IS_PHOTO_PICKER_VISIBLE, editorPhotoPicker?.isPhotoPickerShowing() ?: false @@ -3526,6 +3529,20 @@ class EditPostActivity : LocaleAwareActivity(), EditorFragmentActivity, EditorIm // Start VM, load prompt and populate Editor with content after edit IS ready. val promptId: Int = intent.getIntExtra(EditPostActivityConstants.EXTRA_PROMPT_ID, -1) editorBloggingPromptsViewModel.start(siteModel, promptId) + + updateVoiceContentIfNeeded() + } + + private fun updateVoiceContentIfNeeded() { + // Check if voice content exists and this is a new post for a Gutenberg editor fragment + val content = intent.getStringExtra(EditPostActivityConstants.EXTRA_VOICE_CONTENT) + if (isNewPost && content != null && !isVoiceContentSet) { + val gutenbergFragment = editorFragment as? GutenbergEditorFragment + gutenbergFragment?.let { + isVoiceContentSet = true + it.updateContent(content) + } + } } private fun logTemplateSelection() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivityConstants.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivityConstants.kt index 7fe36f345e5a..77cf11f42b7f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivityConstants.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivityConstants.kt @@ -26,6 +26,7 @@ object EditPostActivityConstants{ const val EXTRA_PAGE_TEMPLATE = "pageTemplate" const val EXTRA_PROMPT_ID = "extraPromptId" const val EXTRA_ENTRY_POINT = "extraEntryPoint" + const val EXTRA_VOICE_CONTENT = "extra_voice_content" const val STATE_KEY_EDITOR_FRAGMENT = "editorFragment" const val STATE_KEY_DROPPED_MEDIA_URIS = "stateKeyDroppedMediaUri" const val STATE_KEY_POST_LOCAL_ID = "stateKeyPostModelLocalId" @@ -40,4 +41,5 @@ object EditPostActivityConstants{ const val STATE_KEY_MEDIA_CAPTURE_PATH = "stateKeyMediaCapturePath" const val STATE_KEY_UNDO = "stateKeyUndo" const val STATE_KEY_REDO = "stateKeyRedo" + const val STATE_KEY_IS_VOICE_CONTENT_SET = "stateKeyIsVoiceContentSet" } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentActionEvent.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentActionEvent.kt new file mode 100644 index 000000000000..543a95b8ed53 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentActionEvent.kt @@ -0,0 +1,10 @@ +package org.wordpress.android.ui.voicetocontent + +import org.wordpress.android.fluxc.model.SiteModel + +sealed class VoiceToContentActionEvent { + data object Dismiss: VoiceToContentActionEvent() + data class LaunchEditPost(val site: SiteModel, val content: String) : VoiceToContentActionEvent() + data class LaunchExternalBrowser(val url: String) : VoiceToContentActionEvent() + data object RequestPermission : VoiceToContentActionEvent() +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt index 7375ae03ffc2..c32a850d0145 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt @@ -6,25 +6,30 @@ import android.content.DialogInterface import android.content.Intent import android.net.Uri import android.os.Bundle +import android.provider.Settings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.viewModels +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint -import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.R -import org.wordpress.android.util.audio.IAudioRecorder.Companion.REQUIRED_RECORDING_PERMISSIONS -import android.provider.Settings -import android.util.Log -import android.widget.FrameLayout -import androidx.compose.material.ExperimentalMaterialApi -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog +import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.ActivityNavigator +import org.wordpress.android.ui.PagePostCreationSourcesDetail +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.voicetocontent.VoiceToContentActionEvent.Dismiss +import org.wordpress.android.ui.voicetocontent.VoiceToContentActionEvent.LaunchEditPost +import org.wordpress.android.ui.voicetocontent.VoiceToContentActionEvent.LaunchExternalBrowser +import org.wordpress.android.ui.voicetocontent.VoiceToContentActionEvent.RequestPermission +import org.wordpress.android.util.audio.IAudioRecorder.Companion.REQUIRED_RECORDING_PERMISSIONS import javax.inject.Inject @AndroidEntryPoint @@ -78,17 +83,16 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() { } override fun onSlide(bottomSheet: View, slideOffset: Float) { - // Handle the slide offset if needed + // no op } }) - // Disable touch interception by the bottom sheet to allow nested scrolling + // Disable touch interception by the bottom sheet to allow nested scrolling for landscape and small screens bottomSheet.setOnTouchListener { _, _ -> false } } // Observe the ViewModel to update the cancelable state of closing on outside touch viewModel.isCancelableOutsideTouch.observe(this) { cancelable -> - Log.i(javaClass.simpleName, "***=> disable outside touch") dialog.setCanceledOnTouchOutside(cancelable) } @@ -105,16 +109,13 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() { } private fun observeViewModel() { - viewModel.requestPermission.observe(viewLifecycleOwner) { - requestAllPermissionsForRecording() - } - - viewModel.dismiss.observe(viewLifecycleOwner) { - dismiss() - } - - viewModel.onIneligibleForVoiceToContent.observe(viewLifecycleOwner) { url -> - launchIneligibleForVoiceToContent(url) + viewModel.actionEvent.observe(viewLifecycleOwner) { actionEvent -> + when(actionEvent) { + is LaunchEditPost -> launchEditPost(actionEvent) + is LaunchExternalBrowser -> launchIneligibleForVoiceToContent(actionEvent) + is RequestPermission -> requestAllPermissionsForRecording() + is Dismiss -> dismiss() + } } } @@ -151,9 +152,21 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() { .show() } - private fun launchIneligibleForVoiceToContent(url: String) { + private fun launchIneligibleForVoiceToContent(event: LaunchExternalBrowser) { context?.let { - activityNavigator.openIneligibleForVoiceToContent(it, url) + activityNavigator.openIneligibleForVoiceToContent(it, event.url) + } + } + + private fun launchEditPost(event: LaunchEditPost) { + activity?.let { + ActivityLauncher.addNewPostWithContentFromAIForResult( + it, + event.site, + false, + PagePostCreationSourcesDetail.POST_FROM_MY_SITE, + event.content + ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt index 68fb9d361a32..088ab86fa345 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt @@ -1,7 +1,6 @@ package org.wordpress.android.ui.voicetocontent import android.content.pm.PackageManager -import android.util.Log import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -17,6 +16,10 @@ import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.fluxc.model.jetpackai.JetpackAIAssistantFeature import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.mysite.SelectedSiteRepository +import org.wordpress.android.ui.voicetocontent.VoiceToContentActionEvent.Dismiss +import org.wordpress.android.ui.voicetocontent.VoiceToContentActionEvent.LaunchEditPost +import org.wordpress.android.ui.voicetocontent.VoiceToContentActionEvent.LaunchExternalBrowser +import org.wordpress.android.ui.voicetocontent.VoiceToContentActionEvent.RequestPermission import org.wordpress.android.ui.voicetocontent.VoiceToContentUIStateType.ERROR import org.wordpress.android.ui.voicetocontent.VoiceToContentUIStateType.INELIGIBLE_FOR_FEATURE import org.wordpress.android.ui.voicetocontent.VoiceToContentUIStateType.INITIALIZING @@ -45,20 +48,14 @@ class VoiceToContentViewModel @Inject constructor( private val prepareVoiceToContentUseCase: PrepareVoiceToContentUseCase, private val logger: VoiceToContentTelemetry ) : ScopedViewModel(mainDispatcher) { - private val _requestPermission = MutableLiveData() - val requestPermission = _requestPermission as LiveData - - private val _dismiss = MutableLiveData() - val dismiss = _dismiss as LiveData - private val _recordingUpdate = MutableLiveData() - val recordingUpdate: LiveData get() = _recordingUpdate - - private val _onIneligibleForVoiceToContent = MutableLiveData() - val onIneligibleForVoiceToContent = _onIneligibleForVoiceToContent as LiveData + val recordingUpdate = _recordingUpdate as LiveData private val _isCancelableOutsideTouch = MutableLiveData(true) - val isCancelableOutsideTouch: LiveData get() = _isCancelableOutsideTouch + val isCancelableOutsideTouch = _isCancelableOutsideTouch as LiveData + + private val _actionEvent = MutableLiveData() + val actionEvent = _actionEvent as LiveData private var isStarted = false @@ -124,8 +121,6 @@ class VoiceToContentViewModel @Inject constructor( stopRecording() } else { updateRecordingData(update) - // todo: Handle other updates if needed when UI is ready, e.g., elapsed time and file size - Log.d("AudioRecorder", "Recording update: $update") } } } @@ -182,16 +177,15 @@ class VoiceToContentViewModel @Inject constructor( when (val result = voiceToContentUseCase.execute(site, file)) { is VoiceToContentResult.Failure -> result.transitionToError() is VoiceToContentResult.Success -> - Log.i(javaClass.simpleName, "***=> result is ${result.content}") + _actionEvent.postValue(LaunchEditPost(site, result.content)) } - _dismiss.postValue(Unit) } } // Permissions private fun onRequestPermission() { logger.track(Stat.VOICE_TO_CONTENT_BUTTON_START_RECORDING_TAPPED) - _requestPermission.postValue(Unit) + _actionEvent.postValue(RequestPermission) } private fun hasAllPermissionsForRecording(): Boolean { @@ -221,7 +215,7 @@ class VoiceToContentViewModel @Inject constructor( private fun onClose() { logger.track(Stat.VOICE_TO_CONTENT_BUTTON_CLOSE_TAPPED) recordingUseCase.endRecordingSession() - _dismiss.postValue(Unit) + _actionEvent.postValue(Dismiss) } private fun onRetryTap() { @@ -232,7 +226,7 @@ class VoiceToContentViewModel @Inject constructor( private fun onLinkTap(url: String?) { logger.track(Stat.VOICE_TO_CONTENT_BUTTON_UPGRADE_TAPPED) url?.let { - _onIneligibleForVoiceToContent.postValue(it) + _actionEvent.postValue(LaunchExternalBrowser(it)) } } diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/AztecEditorFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/AztecEditorFragment.java index 17537f28195d..ee028d2d8ebd 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/AztecEditorFragment.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/AztecEditorFragment.java @@ -1690,6 +1690,11 @@ public void showNotice(String message) { @Override public void onRedoPressed() { } + @Override + public void updateContent(@Nullable CharSequence text) { + // not implemented for Aztec + } + private void onMediaTapped(@NonNull final AztecAttributes attrs, int naturalWidth, int naturalHeight, final MediaType mediaType) { if (mediaType == null || !isAdded()) { diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java b/libs/editor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java index 1a809ad11c6a..18255e7f90d3 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java @@ -57,6 +57,7 @@ public abstract Pair getTitleAndContent(CharSequence public abstract void onUndoPressed(); public abstract void onRedoPressed(); + public abstract void updateContent(CharSequence text); public enum MediaType { diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java index 38504687b0d9..859dadef8272 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java @@ -1096,6 +1096,17 @@ public void setContent(CharSequence text) { getGutenbergContainerFragment().setContent(postContent); } + @Override + public void updateContent(@Nullable CharSequence text) { + if (text == null) { + text = ""; + } + + if (getGutenbergContainerFragment() != null) { + getGutenbergContainerFragment().onContentUpdate(text.toString()); + } + } + public void setJetpackSsoEnabled(boolean jetpackSsoEnabled) { mIsJetpackSsoEnabled = jetpackSsoEnabled; }