diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index b5323b747a03..0b0ba08d2ee0 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -11,6 +11,10 @@ * [***] Block Editor: Added expected double tap for focus behavior for text fields in accessibility mode [https://github.com/WordPress/gutenberg/pull/25384] * [*] Improved My Site layout on devices with big screens. [https://github.com/wordpress-mobile/WordPress-Android/pull/13026] +15.8.1 +----- +* [***] Login: Fixed an issue where some users would be prevented to login with a Jetpack site address. + 15.8 ----- * [**] Reader: Now displaying preview images for more posts when no feature image is set. diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 49f333ed7f3b..1b74daef3347 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -54,9 +54,9 @@ android { if (project.hasProperty("versionName")) { versionName project.property("versionName") } else { - versionName "alpha-248" + versionName "alpha-249" } - versionCode 931 + versionCode 934 minSdkVersion rootProject.minSdkVersion targetSdkVersion rootProject.targetSdkVersion @@ -92,9 +92,9 @@ android { dimension "buildType" // Only set the release version if one isn't provided if (!project.hasProperty("versionName")) { - versionName "15.9-rc-1" + versionName "15.9-rc-2" } - versionCode 930 + versionCode 933 buildConfigField "boolean", "ME_ACTIVITY_AVAILABLE", "false" buildConfigField "boolean", "TENOR_AVAILABLE", "false" buildConfigField "long", "REMOTE_CONFIG_FETCH_INTERVAL", "3600" diff --git a/WordPress/metadata/PlayStoreStrings.po b/WordPress/metadata/PlayStoreStrings.po index d4ae0b3cba54..e1ef93a3d6ff 100644 --- a/WordPress/metadata/PlayStoreStrings.po +++ b/WordPress/metadata/PlayStoreStrings.po @@ -11,21 +11,20 @@ msgstr "" "Project-Id-Version: Release Notes & Play Store Descriptions\n" #. translators: Release notes for this version to be displayed in the Play Store. Limit to 500 characters including spaces and commas! -msgctxt "release_note_158" +msgctxt "release_note_159" msgid "" -"15.8:\n" -"- Block editor enhancements: Added full-width and wide alignment support for Group, Cover, and Image blocks; added support for the Rounded style in the Image block; and resolved an issue where the toolbar behaved unexpectedly when adding a block.\n" -"- Reader update: Preview images are now displayed on posts when no featured image is set.\n" -"\n" +"15.9:\n" +"- Block editor: Edit unsupported blocks by enabling Jetpack SSO from the missing block alert. Use the link picker to add URLs to posts. Double-tap text fields in accessibility mode as expected.\n" +"- Improved My Site layout for large-screen devices.\n" "\n" msgstr "" -msgctxt "release_note_157" +msgctxt "release_note_158" msgid "" -"15.7:\n" -"Improvements in the Reader: A new Discover tab you can tailor to your interests and an “(un)follow” button to the site and topic detail screens.\n" +"15.8:\n" +"- Block editor enhancements: Added full-width and wide alignment support for Group, Cover, and Image blocks; added support for the Rounded style in the Image block; and resolved an issue where the toolbar behaved unexpectedly when adding a block.\n" +"- Reader update: Preview images are now displayed on posts when no featured image is set.\n" "\n" -"Block Editor enhancements: A new Pullquote block that lets you add emphasis to a chosen piece of text. Any changes made in menu sliders are now also immediately reflected in block settings.\n" "\n" msgstr "" diff --git a/WordPress/metadata/release_notes.txt b/WordPress/metadata/release_notes.txt index 382bd5bc5c74..2bc873408b9a 100644 --- a/WordPress/metadata/release_notes.txt +++ b/WordPress/metadata/release_notes.txt @@ -1,8 +1,3 @@ -* [*] Block Editor: Add message that mentions are unavailable when device is offline. [https://github.com/wordpress-mobile/WordPress-Android/pull/12968] -* [**] Block Editor: Increase tap-target of primary action on unsupported blocks. [https://github.com/wordpress-mobile/gutenberg-mobile/pull/2608] -* [***] Block Editor: On Jetpack connected sites, Unsupported Block Editor can be enabled via enabling Jetpack SSO setting directly from within the missing block alert. [https://github.com/wordpress-mobile/gutenberg-mobile/pull/2610] -* [***] Block Editor: Add support for selecting user's post when configuring the link [https://github.com/wordpress-mobile/gutenberg-mobile/pull/2484] -* [**] Block Editor: Fix for a crash that can occur when long pressing selected images [https://github.com/wordpress-mobile/AztecEditor-Android/pull/924] -* [***] Block Editor: Added expected double tap for focus behavior for text fields in accessibility mode [https://github.com/WordPress/gutenberg/pull/25384] -* [*] Improved My Site layout on devices with big screens. [https://github.com/wordpress-mobile/WordPress-Android/pull/13026] +- Block editor: Edit unsupported blocks by enabling Jetpack SSO from the missing block alert. Use the link picker to add URLs to posts. Double-tap text fields in accessibility mode as expected. +- Improved My Site layout for large-screen devices. diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDiscoverCardsTable.kt b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDiscoverCardsTable.kt index a598767b0951..0d5cf72dc61a 100644 --- a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDiscoverCardsTable.kt +++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDiscoverCardsTable.kt @@ -21,14 +21,9 @@ object ReaderDiscoverCardsTable { db.execSQL("DROP TABLE IF EXISTS tbl_discover_cards") } - fun reset() { - reset(getWritableDb()) - } - - fun reset(db: SQLiteDatabase) { - AppLog.i(AppLog.T.READER, "resetting ReaderDiscoverCardsTable") - dropTables(db) - createTable(db) + fun clear() { + AppLog.i(AppLog.T.READER, "clearing ReaderDiscoverCardsTable") + getWritableDb().delete(DISCOVER_CARDS_TABLE, null, null) } private fun getReadableDb(): SQLiteDatabase { 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 f06a4c15c54d..1f0fc2ff57ef 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java @@ -204,6 +204,10 @@ private static Intent createShowPhotoPickerIntent(Context context, return intent; } + /** + * Use {@link org.wordpress.android.ui.photopicker.MediaPickerLauncher::showStockMediaPickerForResult} instead + */ + @Deprecated public static void showStockMediaPickerForResult(Activity activity, @NonNull SiteModel site, int requestCode) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java index e6c3abb647e3..1e1648e803e4 100755 --- a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java @@ -1013,8 +1013,8 @@ private void doAddMediaItemClicked(@NonNull AddMenuItem item) { mMediaPickerLauncher.showFilePicker(this); break; case ITEM_CHOOSE_STOCK_MEDIA: - ActivityLauncher.showStockMediaPickerForResult(this, - mSite, RequestCodes.STOCK_MEDIA_PICKER_MULTI_SELECT); + mMediaPickerLauncher.showStockMediaPickerForResult(this, + mSite, RequestCodes.STOCK_MEDIA_PICKER_MULTI_SELECT, true); break; case ITEM_CHOOSE_GIF: ActivityLauncher.showGifPickerForResult(this, mSite, RequestCodes.GIF_PICKER_MULTI_SELECT); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaItem.kt index db27d77cdec8..1424947113d6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaItem.kt @@ -4,7 +4,6 @@ import android.net.Uri import android.os.Parcelable import kotlinx.android.parcel.Parcelize import org.wordpress.android.util.UriWrapper -import java.lang.IllegalArgumentException data class MediaItem( val identifier: Identifier, @@ -17,6 +16,7 @@ data class MediaItem( sealed class Identifier { data class LocalUri(val value: UriWrapper) : Identifier() data class RemoteId(val value: Long) : Identifier() + data class StockMediaIdentifier(val url: String?, val name: String?, val title: String?) : Identifier() fun toParcel() = Parcel((this as? LocalUri)?.value?.uri, (this as? RemoteId)?.value) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaLoader.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaLoader.kt deleted file mode 100644 index c1f4a6f7df0b..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaLoader.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.wordpress.android.ui.mediapicker - -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import org.wordpress.android.ui.mediapicker.MediaLoader.LoadAction.ClearFilter -import org.wordpress.android.ui.mediapicker.MediaLoader.LoadAction.Filter -import org.wordpress.android.ui.mediapicker.MediaLoader.LoadAction.NextPage -import org.wordpress.android.ui.mediapicker.MediaLoader.LoadAction.Refresh -import org.wordpress.android.ui.mediapicker.MediaLoader.LoadAction.Start -import org.wordpress.android.ui.mediapicker.MediaSource.MediaLoadingResult -import org.wordpress.android.ui.mediapicker.MediaSource.MediaLoadingResult.Failure -import org.wordpress.android.ui.mediapicker.MediaSource.MediaLoadingResult.Success -import org.wordpress.android.util.LocaleManagerWrapper - -data class MediaLoader( - private val mediaSource: MediaSource, - private val localeManagerWrapper: LocaleManagerWrapper, - private val allowedTypes: Set -) { - suspend fun loadMedia(actions: Channel): Flow { - return flow { - var state = DomainModel() - for (loadAction in actions) { - when (loadAction) { - is Start -> { - if (state.domainItems.isEmpty() || state.error != null) { - state = buildDomainModel(mediaSource.load(allowedTypes), state) - emit(state) - } - } - is Refresh -> { - if (loadAction.forced || state.domainItems.isEmpty()) { - emit(state.copy(isLoading = true)) - state = buildDomainModel(mediaSource.load(allowedTypes, forced = loadAction.forced), state) - emit(state) - } - } - is NextPage -> { - val load = mediaSource.load(mediaTypes = allowedTypes, loadMore = true) - state = buildDomainModel(load, state) - emit(state) - } - is Filter -> { - state = state.copy( - filter = loadAction.filter, - domainItems = mediaSource.get(allowedTypes, loadAction.filter) - ) - emit(state) - } - is ClearFilter -> { - state = state.copy(filter = null, domainItems = mediaSource.get(allowedTypes, null)) - emit(state) - } - } - } - } - } - - private suspend fun buildDomainModel( - partialResult: MediaLoadingResult, - state: DomainModel - ): DomainModel { - return when (partialResult) { - is Success -> state.copy( - isLoading = false, - error = null, - hasMore = partialResult.hasMore, - domainItems = mediaSource.get(allowedTypes, state.filter) - ) - is Failure -> state.copy(isLoading = false, error = partialResult.message) - } - } - - sealed class LoadAction { - data class Start(val filter: String? = null) : LoadAction() - data class Refresh(val forced: Boolean) : LoadAction() - data class Filter(val filter: String) : LoadAction() - object NextPage : LoadAction() - object ClearFilter : LoadAction() - } - - data class DomainModel( - val domainItems: List = listOf(), - val error: String? = null, - val hasMore: Boolean = false, - val isFilteredResult: Boolean = false, - val filter: String? = null, - val isLoading: Boolean = false - ) -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActivity.kt index d20ab6acc854..a1a8e88b221d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActivity.kt @@ -7,7 +7,6 @@ import android.net.Uri import android.os.Bundle import android.text.TextUtils import android.view.MenuItem -import android.widget.Toast import androidx.fragment.app.FragmentTransaction import kotlinx.android.synthetic.main.toolbar_main.* import org.wordpress.android.R @@ -46,10 +45,6 @@ import org.wordpress.android.ui.photopicker.MediaPickerConstants.EXTRA_MEDIA_URI import org.wordpress.android.ui.photopicker.MediaPickerConstants.LOCAL_POST_ID import org.wordpress.android.ui.posts.EMPTY_LOCAL_POST_ID import org.wordpress.android.ui.posts.FeaturedImageHelper -import org.wordpress.android.ui.posts.FeaturedImageHelper.EnqueueFeaturedImageResult.FILE_NOT_FOUND -import org.wordpress.android.ui.posts.FeaturedImageHelper.EnqueueFeaturedImageResult.INVALID_POST_ID -import org.wordpress.android.ui.posts.FeaturedImageHelper.EnqueueFeaturedImageResult.SUCCESS -import org.wordpress.android.ui.posts.FeaturedImageHelper.TrackableEvent.IMAGE_PICKED import org.wordpress.android.ui.posts.editor.ImageEditorTracker import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.AppLog @@ -235,39 +230,11 @@ class MediaPickerActivity : LocaleAwareActivity(), MediaPickerListener { ) { // if user chose a featured image, we need to upload it and return the uploaded media object if (mediaPickerSetup.queueResults) { - val mediaUri = mediaUris[0] - val mimeType = contentResolver.getType(mediaUri) - featuredImageHelper.trackFeaturedImageEvent( - IMAGE_PICKED, - localPostId - ) - WPMediaUtils.fetchMediaAndDoNext( - this, mediaUri - ) { uri -> - val queueImageResult = featuredImageHelper - .queueFeaturedImageForUpload( - localPostId, site!!, uri, - mimeType - ) - when (queueImageResult) { - FILE_NOT_FOUND -> Toast.makeText( - applicationContext, - R.string.file_not_found, Toast.LENGTH_SHORT - ) - .show() - INVALID_POST_ID -> Toast.makeText( - applicationContext, - R.string.error_generic, Toast.LENGTH_SHORT - ) - .show() - SUCCESS -> { - } - } - val intent = Intent() - .putExtra(EXTRA_MEDIA_QUEUED, true) - setResult(Activity.RESULT_OK, intent) - finish() - } + val intent = Intent() + .putExtra(EXTRA_MEDIA_QUEUED, true) + .putExtra(EXTRA_MEDIA_URIS, convertUrisListToStringArray(mediaUris)) + setResult(Activity.RESULT_OK, intent) + finish() } else { val intent = Intent() .putExtra(EXTRA_MEDIA_URIS, convertUrisListToStringArray(mediaUris)) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt index 6adad4d06439..9a48277e0d0f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt @@ -1,6 +1,7 @@ package org.wordpress.android.ui.mediapicker import android.Manifest.permission +import android.app.Activity import android.content.Intent.ACTION_GET_CONTENT import android.content.Intent.ACTION_OPEN_DOCUMENT import android.os.Bundle @@ -13,6 +14,8 @@ import android.view.MenuItem import android.view.MenuItem.OnActionExpandListener import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AlertDialog.Builder import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.fragment.app.Fragment @@ -20,16 +23,21 @@ import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.media_picker_fragment.* import org.wordpress.android.R +import org.wordpress.android.R.layout +import org.wordpress.android.R.string import org.wordpress.android.WordPress import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.RequestCodes import org.wordpress.android.ui.media.MediaPreviewActivity +import org.wordpress.android.ui.mediapicker.MediaItem.Identifier import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerIconType.ANDROID_CHOOSE_FROM_DEVICE import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerIconType.WP_STORIES_CAPTURE -import org.wordpress.android.ui.mediapicker.MediaItem.Identifier import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.ActionModeUiModel import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.FabUiModel import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.PermissionsRequested.CAMERA @@ -38,12 +46,19 @@ import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.PhotoListUiMode import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.PhotoListUiModel.Data import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.PhotoListUiModel.Empty import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.PhotoListUiModel.Hidden +import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.ProgressDialogUiModel +import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.ProgressDialogUiModel.Visible import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.SearchUiModel import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.SoftAskViewUiModel +import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.util.AccessibilityUtils import org.wordpress.android.util.AniUtils import org.wordpress.android.util.AniUtils.Duration.MEDIUM +import org.wordpress.android.util.SnackbarItem +import org.wordpress.android.util.SnackbarItem.Action +import org.wordpress.android.util.SnackbarItem.Info +import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.WPMediaUtils import org.wordpress.android.util.WPPermissionUtils import org.wordpress.android.util.WPSwipeToRefreshHelper @@ -83,6 +98,7 @@ class MediaPickerFragment : Fragment() { val mimeTypes: List, val allowMultipleSelection: Boolean ) : MediaPickerAction() + data class OpenCameraForWPStories(val allowMultipleSelection: Boolean) : MediaPickerAction() } @@ -90,6 +106,7 @@ class MediaPickerFragment : Fragment() { data class ChooseFromAndroidDevice( val allowedTypes: Set ) : MediaPickerIcon(ANDROID_CHOOSE_FROM_DEVICE) + object WpStoriesCapture : MediaPickerIcon(WP_STORIES_CAPTURE) fun toBundle(bundle: Bundle) { @@ -133,6 +150,7 @@ class MediaPickerFragment : Fragment() { @Inject lateinit var tenorFeatureConfig: TenorFeatureConfig @Inject lateinit var imageManager: ImageManager @Inject lateinit var viewModelFactory: ViewModelProvider.Factory + @Inject lateinit var snackbarSequencer: SnackbarSequencer private lateinit var viewModel: MediaPickerViewModel override fun onCreate(savedInstanceState: Bundle?) { @@ -168,8 +186,6 @@ class MediaPickerFragment : Fragment() { ?.map { Identifier.fromParcel(it) } } } - recycler.setEmptyView(actionable_empty_view) - recycler.setHasFixedSize(true) val layoutManager = GridLayoutManager( activity, @@ -181,6 +197,8 @@ class MediaPickerFragment : Fragment() { } recycler.layoutManager = layoutManager + recycler.setEmptyView(actionable_empty_view) + recycler.setHasFixedSize(true) val swipeToRefreshHelper = WPSwipeToRefreshHelper.buildSwipeToRefreshHelper(pullToRefresh) { viewModel.onPullToRefresh() @@ -249,6 +267,19 @@ class MediaPickerFragment : Fragment() { } } }) + viewModel.onExit.observe(viewLifecycleOwner, Observer { + it?.applyIfNotHandled { + val activity = requireActivity() + activity.setResult(Activity.RESULT_CANCELED) + activity.finish() + } + }) + viewModel.onSnackbarMessage.observe(viewLifecycleOwner, Observer { + it?.getContentIfNotHandled()?.let { messageHolder -> + showSnackbar(messageHolder) + } + }) + setupProgressDialog() viewModel.start(selectedIds, mediaPickerSetup, lastTappedIcon, site) } @@ -270,6 +301,7 @@ class MediaPickerFragment : Fragment() { if (uiState.searchUiModel is SearchUiModel.Expanded && !searchMenuItem.isActionViewExpanded) { searchMenuItem.expandActionView() searchView.setQuery(uiState.searchUiModel.filter, true) + searchView.setOnCloseListener { !uiState.searchUiModel.closeable } } else if (uiState.searchUiModel is SearchUiModel.Collapsed && searchMenuItem.isActionViewExpanded) { searchMenuItem.collapseActionView() } @@ -343,36 +375,41 @@ class MediaPickerFragment : Fragment() { private fun setupPhotoList(uiModel: PhotoListUiModel) { when (uiModel) { is Data -> { - recycler.setEmptyViewIfNull(actionable_empty_view) - if (recycler.adapter == null) { - recycler.adapter = MediaPickerAdapter( - imageManager - ) - } - val adapter = recycler.adapter as MediaPickerAdapter - - (recycler.layoutManager as? GridLayoutManager)?.spanSizeLookup = - object : GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(position: Int) = if (uiModel.items[position].fullWidthItem) { - NUM_COLUMNS - } else { - 1 - } - } - val recyclerViewState = recycler.layoutManager?.onSaveInstanceState() - adapter.loadData(uiModel.items) - recycler.layoutManager?.onRestoreInstanceState(recyclerViewState) + actionable_empty_view.visibility = View.GONE + recycler.visibility = View.VISIBLE + setupAdapter(uiModel.items) } Empty -> { - recycler.setEmptyView(actionable_empty_view) + actionable_empty_view.visibility = View.VISIBLE + recycler.visibility = View.INVISIBLE + setupAdapter(listOf()) } Hidden -> { - recycler.setEmptyView(null) actionable_empty_view.visibility = View.GONE + recycler.visibility = View.INVISIBLE } } } + private fun setupAdapter(items: List) { + if (recycler.adapter == null) { + recycler.adapter = MediaPickerAdapter( + imageManager + ) + } + val adapter = recycler.adapter as MediaPickerAdapter + + (recycler.layoutManager as? GridLayoutManager)?.spanSizeLookup = + object : SpanSizeLookup() { + override fun getSpanSize(position: Int) = if (items[position].fullWidthItem) { + NUM_COLUMNS + } else { + 1 + } + } + adapter.loadData(items) + } + private fun setupFab(fabUiModel: FabUiModel) { if (fabUiModel.show) { wp_stories_take_picture.show() @@ -384,6 +421,55 @@ class MediaPickerFragment : Fragment() { } } + private fun setupProgressDialog() { + var progressDialog: AlertDialog? = null + viewModel.uiState.observe(viewLifecycleOwner, Observer { + it?.progressDialogUiModel?.apply { + when (this) { + is Visible -> { + if (progressDialog == null || progressDialog?.isShowing == false) { + val builder: Builder = MaterialAlertDialogBuilder(requireContext()) + builder.setTitle(string.media_uploading_stock_library_photo) + builder.setView(layout.media_picker_progress_dialog) + builder.setNegativeButton( + string.cancel + ) { _, _ -> this.cancelAction() } + builder.setOnCancelListener { this.cancelAction() } + builder.setCancelable(true) + progressDialog = builder.show() + } + } + ProgressDialogUiModel.Hidden -> { + progressDialog?.let { dialog -> + if (dialog.isShowing) { + dialog.dismiss() + } + } + } + } + } + }) + } + + private fun showSnackbar(holder: SnackbarMessageHolder) { + snackbarSequencer.enqueue( + SnackbarItem( + Info( + view = coordinator, + textRes = holder.message, + duration = Snackbar.LENGTH_LONG + ), + holder.buttonTitle?.let { + Action( + textRes = holder.buttonTitle, + clickListener = View.OnClickListener { holder.buttonAction() } + ) + }, + dismissCallback = { _, _ -> holder.onDismissAction() } + ) + ) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) viewModel.lastTappedIcon?.toBundle(outState) @@ -422,7 +508,7 @@ class MediaPickerFragment : Fragment() { } private fun requestStoragePermission() { - val permissions = arrayOf(permission.WRITE_EXTERNAL_STORAGE) + val permissions = arrayOf(permission.WRITE_EXTERNAL_STORAGE, permission.READ_EXTERNAL_STORAGE) requestPermissions( permissions, WPPermissionUtils.PHOTO_PICKER_STORAGE_PERMISSION_REQUEST_CODE ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerSetup.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerSetup.kt index e40c6067b761..2c61f89f919f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerSetup.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerSetup.kt @@ -14,6 +14,7 @@ data class MediaPickerSetup( val systemPickerEnabled: Boolean, val editingEnabled: Boolean, val queueResults: Boolean, + val defaultSearchView: Boolean, @StringRes val title: Int ) { enum class DataSource { @@ -29,6 +30,7 @@ data class MediaPickerSetup( bundle.putBoolean(KEY_SYSTEM_PICKER_ENABLED, systemPickerEnabled) bundle.putBoolean(KEY_EDITING_ENABLED, editingEnabled) bundle.putBoolean(KEY_QUEUE_RESULTS, queueResults) + bundle.putBoolean(KEY_DEFAULT_SEARCH_VIEW, defaultSearchView) bundle.putInt(KEY_TITLE, title) } @@ -41,6 +43,7 @@ data class MediaPickerSetup( intent.putExtra(KEY_SYSTEM_PICKER_ENABLED, systemPickerEnabled) intent.putExtra(KEY_EDITING_ENABLED, editingEnabled) intent.putExtra(KEY_QUEUE_RESULTS, queueResults) + intent.putExtra(KEY_DEFAULT_SEARCH_VIEW, defaultSearchView) intent.putExtra(KEY_TITLE, title) } @@ -53,6 +56,7 @@ data class MediaPickerSetup( private const val KEY_SYSTEM_PICKER_ENABLED = "key_system_picker_enabled" private const val KEY_EDITING_ENABLED = "key_editing_enabled" private const val KEY_QUEUE_RESULTS = "key_queue_results" + private const val KEY_DEFAULT_SEARCH_VIEW = "key_default_search_view" private const val KEY_TITLE = "key_title" fun fromBundle(bundle: Bundle): MediaPickerSetup { @@ -68,6 +72,7 @@ data class MediaPickerSetup( val systemPickerEnabled = bundle.getBoolean(KEY_SYSTEM_PICKER_ENABLED) val editingEnabled = bundle.getBoolean(KEY_EDITING_ENABLED) val queueResults = bundle.getBoolean(KEY_QUEUE_RESULTS) + val defaultSearchView = bundle.getBoolean(KEY_DEFAULT_SEARCH_VIEW) val title = bundle.getInt(KEY_TITLE) return MediaPickerSetup( dataSource, @@ -78,6 +83,7 @@ data class MediaPickerSetup( systemPickerEnabled, editingEnabled, queueResults, + defaultSearchView, title ) } @@ -95,6 +101,7 @@ data class MediaPickerSetup( val systemPickerEnabled = intent.getBooleanExtra(KEY_SYSTEM_PICKER_ENABLED, false) val editingEnabled = intent.getBooleanExtra(KEY_SYSTEM_PICKER_ENABLED, false) val queueResults = intent.getBooleanExtra(KEY_QUEUE_RESULTS, false) + val defaultSearchView = intent.getBooleanExtra(KEY_DEFAULT_SEARCH_VIEW, false) val title = intent.getIntExtra(KEY_TITLE, R.string.photo_picker_photo_or_video_title) return MediaPickerSetup( dataSource, @@ -105,6 +112,7 @@ data class MediaPickerSetup( systemPickerEnabled, editingEnabled, queueResults, + defaultSearchView, title ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModel.kt index ad55ce9da631..32e083fddd60 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModel.kt @@ -4,7 +4,9 @@ import android.Manifest.permission import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -12,16 +14,12 @@ import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.analytics.AnalyticsTracker.Stat.MEDIA_PICKER_OPEN_WP_STORIES_CAPTURE import org.wordpress.android.analytics.AnalyticsTracker.Stat.MEDIA_PICKER_PREVIEW_OPENED -import org.wordpress.android.analytics.AnalyticsTracker.Stat.MEDIA_PICKER_RECENT_MEDIA_SELECTED import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.utils.MimeTypes import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.mediapicker.MediaItem.Identifier import org.wordpress.android.ui.mediapicker.MediaItem.Identifier.LocalUri -import org.wordpress.android.ui.mediapicker.MediaLoader.DomainModel -import org.wordpress.android.ui.mediapicker.MediaLoader.LoadAction -import org.wordpress.android.ui.mediapicker.MediaLoader.LoadAction.NextPage import org.wordpress.android.ui.mediapicker.MediaPickerFragment.ChooserContext import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerAction import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerAction.OpenCameraForWPStories @@ -34,16 +32,27 @@ import org.wordpress.android.ui.mediapicker.MediaPickerUiItem.FileItem import org.wordpress.android.ui.mediapicker.MediaPickerUiItem.PhotoItem import org.wordpress.android.ui.mediapicker.MediaPickerUiItem.ToggleAction import org.wordpress.android.ui.mediapicker.MediaPickerUiItem.VideoItem +import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.ProgressDialogUiModel.Hidden +import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.ProgressDialogUiModel.Visible import org.wordpress.android.ui.mediapicker.MediaType.AUDIO import org.wordpress.android.ui.mediapicker.MediaType.DOCUMENT import org.wordpress.android.ui.mediapicker.MediaType.IMAGE import org.wordpress.android.ui.mediapicker.MediaType.VIDEO +import org.wordpress.android.ui.mediapicker.insert.MediaInsertHandler +import org.wordpress.android.ui.mediapicker.insert.MediaInsertHandler.InsertModel +import org.wordpress.android.ui.mediapicker.insert.MediaInsertHandlerFactory +import org.wordpress.android.ui.mediapicker.loader.MediaLoader +import org.wordpress.android.ui.mediapicker.loader.MediaLoader.DomainModel +import org.wordpress.android.ui.mediapicker.loader.MediaLoader.LoadAction +import org.wordpress.android.ui.mediapicker.loader.MediaLoader.LoadAction.NextPage +import org.wordpress.android.ui.mediapicker.loader.MediaLoaderFactory +import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.photopicker.PermissionsHandler import org.wordpress.android.ui.utils.UiString import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.ui.utils.UiString.UiStringResWithParams import org.wordpress.android.ui.utils.UiString.UiStringText import org.wordpress.android.util.LocaleManagerWrapper -import org.wordpress.android.util.MediaUtils import org.wordpress.android.util.MediaUtilsWrapper import org.wordpress.android.util.UriWrapper import org.wordpress.android.util.WPPermissionUtils @@ -61,6 +70,7 @@ class MediaPickerViewModel @Inject constructor( @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, private val mediaLoaderFactory: MediaLoaderFactory, + private val mediaInsertHandlerFactory: MediaInsertHandlerFactory, private val analyticsUtilsWrapper: AnalyticsUtilsWrapper, private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, private val permissionsHandler: PermissionsHandler, @@ -69,6 +79,7 @@ class MediaPickerViewModel @Inject constructor( private val resourceProvider: ResourceProvider ) : ScopedViewModel(mainDispatcher) { private lateinit var mediaLoader: MediaLoader + private lateinit var mediaInsertHandler: MediaInsertHandler private val loadActions = Channel() private val _navigateToPreview = MutableLiveData>() private val _navigateToEdit = MutableLiveData>>() @@ -79,11 +90,16 @@ class MediaPickerViewModel @Inject constructor( private val _onPermissionsRequested = MutableLiveData>() private val _softAskRequest = MutableLiveData() private val _searchExpanded = MutableLiveData() + private val _showProgressDialog = MutableLiveData() + private val _onExit = MutableLiveData>() + private val _onSnackbarMessage = MutableLiveData>() val onNavigateToPreview: LiveData> = _navigateToPreview val onNavigateToEdit: LiveData>> = _navigateToEdit val onInsert: LiveData>> = _onInsert val onIconClicked: LiveData> = _onIconClicked + val onExit: LiveData> = _onExit + val onSnackbarMessage: LiveData> = _onSnackbarMessage val onPermissionsRequested: LiveData> = _onPermissionsRequested @@ -91,8 +107,9 @@ class MediaPickerViewModel @Inject constructor( _domainModel.distinct(), _selectedIds.distinct(), _softAskRequest, - _searchExpanded - ) { domainModel, selectedIds, softAskRequest, searchExpanded -> + _searchExpanded, + _showProgressDialog.distinct() + ) { domainModel, selectedIds, softAskRequest, searchExpanded, progressDialogUiModel -> MediaPickerUiState( buildUiModel(domainModel, selectedIds, softAskRequest), buildSoftAskView(softAskRequest), @@ -102,13 +119,14 @@ class MediaPickerViewModel @Inject constructor( buildActionModeUiModel(selectedIds, domainModel?.domainItems), buildSearchUiModel(softAskRequest?.let { !it.show } ?: true, domainModel?.filter, searchExpanded), !domainModel?.domainItems.isNullOrEmpty() && domainModel?.isLoading == true, - buildBrowseMenuUiModel(softAskRequest, searchExpanded) + buildBrowseMenuUiModel(softAskRequest, searchExpanded), + progressDialogUiModel ?: Hidden ) } private fun buildSearchUiModel(isVisible: Boolean, filter: String?, searchExpanded: Boolean?): SearchUiModel { return when { - searchExpanded == true -> SearchUiModel.Expanded(filter ?: "") + searchExpanded == true -> SearchUiModel.Expanded(filter ?: "", !mediaPickerSetup.defaultSearchView) isVisible -> SearchUiModel.Collapsed else -> SearchUiModel.Hidden } @@ -132,7 +150,7 @@ class MediaPickerViewModel @Inject constructor( val data = domainModel?.domainItems return if (null != softAskRequest && softAskRequest.show) { PhotoListUiModel.Hidden - } else if (data != null) { + } else if (data != null && data.isNotEmpty()) { val uiItems = data.map { val showOrderCounter = mediaPickerSetup.canMultiselect val toggleAction = ToggleAction(it.identifier, showOrderCounter, this::toggleItem) @@ -264,6 +282,7 @@ class MediaPickerViewModel @Inject constructor( this.site = site if (_domainModel.value == null) { this.mediaLoader = mediaLoaderFactory.build(mediaPickerSetup, site) + this.mediaInsertHandler = mediaInsertHandlerFactory.build(mediaPickerSetup, site) launch(bgDispatcher) { mediaLoader.loadMedia(loadActions).collect { domainModel -> withContext(mainDispatcher) { @@ -275,6 +294,9 @@ class MediaPickerViewModel @Inject constructor( loadActions.send(LoadAction.Start()) } } + if (mediaPickerSetup.defaultSearchView) { + _searchExpanded.postValue(true) + } } fun numSelected(): Int { @@ -323,21 +345,45 @@ class MediaPickerViewModel @Inject constructor( fun performInsertAction() { val ids = selectedIdentifiers() - _onInsert.value = Event(ids) - val isMultiselection = ids.size > 1 - for (identifier in ids) { - if (identifier is LocalUri) { - val isVideo = MediaUtils.isVideo(identifier.toString()) - val properties = analyticsUtilsWrapper.getMediaProperties( - isVideo, - identifier.value, - null - ) - properties["is_part_of_multiselection"] = isMultiselection - if (isMultiselection) { - properties["number_of_media_selected"] = ids.size + var job: Job? = null + job = launch { + var progressDialogJob: Job? = null + mediaInsertHandler.insertMedia(ids).collect { + when (it) { + is InsertModel.Progress -> { + progressDialogJob = launch { + delay(100) + _showProgressDialog.value = Visible(R.string.media_uploading_stock_library_photo) { + job?.cancel() + _showProgressDialog.value = Hidden + } + } + } + is InsertModel.Error -> { + val message = if (it.error.isNotEmpty()) { + UiStringResWithParams( + R.string.media_insert_failed_with_reason, + listOf(UiStringText(it.error)) + ) + } else { + UiStringRes(R.string.media_insert_failed) + } + _onSnackbarMessage.value = Event( + SnackbarMessageHolder( + message + ) + ) + progressDialogJob?.cancel() + job = null + _showProgressDialog.value = Hidden + } + is InsertModel.Success -> { + progressDialogJob?.cancel() + job = null + _showProgressDialog.value = Hidden + _onInsert.value = Event(it.identifiers) + } } - analyticsTrackerWrapper.track(MEDIA_PICKER_RECENT_MEDIA_SELECTED, properties) } } } @@ -412,15 +458,23 @@ class MediaPickerViewModel @Inject constructor( if (softAskRequest != null && softAskRequest.show) { val appName = "${resourceProvider.getString(R.string.app_name)}" val label = if (softAskRequest.isAlwaysDenied) { - val permissionName = ("${ + val writePermission = ("${ WPPermissionUtils.getPermissionName( resourceProvider, permission.WRITE_EXTERNAL_STORAGE ) }") + val readPermission = ("${ + WPPermissionUtils.getPermissionName( + resourceProvider, + permission.READ_EXTERNAL_STORAGE + ) + }") String.format( - resourceProvider.getString(R.string.photo_picker_soft_ask_permissions_denied), appName, - permissionName + resourceProvider.getString(R.string.media_picker_soft_ask_permissions_denied), + appName, + writePermission, + readPermission ) } else { String.format( @@ -450,9 +504,13 @@ class MediaPickerViewModel @Inject constructor( } fun onSearchCollapsed() { - _searchExpanded.value = false - launch(bgDispatcher) { - loadActions.send(LoadAction.ClearFilter) + if (!mediaPickerSetup.defaultSearchView) { + _searchExpanded.value = false + launch(bgDispatcher) { + loadActions.send(LoadAction.ClearFilter) + } + } else { + _onExit.postValue(Event(Unit)) } } @@ -467,7 +525,8 @@ class MediaPickerViewModel @Inject constructor( val actionModeUiModel: ActionModeUiModel, val searchUiModel: SearchUiModel, val isRefreshing: Boolean, - val browseMenuUiModel: BrowseMenuUiModel + val browseMenuUiModel: BrowseMenuUiModel, + val progressDialogUiModel: ProgressDialogUiModel ) sealed class PhotoListUiModel { @@ -498,7 +557,7 @@ class MediaPickerViewModel @Inject constructor( sealed class SearchUiModel { object Collapsed : SearchUiModel() - data class Expanded(val filter: String) : SearchUiModel() + data class Expanded(val filter: String, val closeable: Boolean = true) : SearchUiModel() object Hidden : SearchUiModel() } @@ -517,4 +576,9 @@ class MediaPickerViewModel @Inject constructor( val isCounterBadgeVisible: Boolean = false, val counterBadgeValue: Int = 1 ) + + sealed class ProgressDialogUiModel { + object Hidden : ProgressDialogUiModel() + data class Visible(val title: Int, val cancelAction: () -> Unit) : ProgressDialogUiModel() + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaSource.kt deleted file mode 100644 index 0aba8eba086b..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaSource.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.wordpress.android.ui.mediapicker - -interface MediaSource { - suspend fun load( - mediaTypes: Set, - forced: Boolean = false, - loadMore: Boolean = false - ): MediaLoadingResult - - suspend fun get(mediaTypes: Set, filter: String? = null): List - - sealed class MediaLoadingResult { - data class Success(val hasMore: Boolean = false) : MediaLoadingResult() - data class Failure(val message: String) : MediaLoadingResult() - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/DeviceListInsertUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/DeviceListInsertUseCase.kt new file mode 100644 index 000000000000..41bebf92c864 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/DeviceListInsertUseCase.kt @@ -0,0 +1,76 @@ +package org.wordpress.android.ui.mediapicker.insert + +import kotlinx.coroutines.flow.flow +import org.wordpress.android.analytics.AnalyticsTracker.Stat.MEDIA_PICKER_RECENT_MEDIA_SELECTED +import org.wordpress.android.ui.mediapicker.MediaItem.Identifier +import org.wordpress.android.ui.mediapicker.MediaItem.Identifier.LocalUri +import org.wordpress.android.ui.mediapicker.insert.MediaInsertHandler.InsertModel +import org.wordpress.android.util.UriWrapper +import org.wordpress.android.util.WPMediaUtilsWrapper +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import org.wordpress.android.util.analytics.AnalyticsUtilsWrapper +import javax.inject.Inject + +class DeviceListInsertUseCase( + private val wpMediaUtilsWrapper: WPMediaUtilsWrapper, + private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, + private val analyticsUtilsWrapper: AnalyticsUtilsWrapper, + private val queueResults: Boolean +) : MediaInsertUseCase { + override suspend fun insert(identifiers: List) = flow { + val localUris = identifiers.mapNotNull { it as? LocalUri } + val result = if (queueResults) { + emit(InsertModel.Progress) + var failed = false + val fetchedUris = localUris.mapNotNull { localUri -> + val fetchedUri = wpMediaUtilsWrapper.fetchMedia(localUri.value.uri) + if (fetchedUri == null) { + failed = true + } + fetchedUri + } + if (failed) { + InsertModel.Error("Failed to fetch local media") + } else { + InsertModel.Success(fetchedUris.map { LocalUri(UriWrapper(it)) }) + } + } else { + trackLocalItemsSelected(localUris) + InsertModel.Success(localUris) + } + emit(result) + } + + private fun trackLocalItemsSelected(identifiers: List) { + val isMultiselection = identifiers.size > 1 + for (identifier in identifiers) { + val isVideo = org.wordpress.android.util.MediaUtils.isVideo(identifier.toString()) + val properties = analyticsUtilsWrapper.getMediaProperties( + isVideo, + identifier.value, + null + ) + properties["is_part_of_multiselection"] = isMultiselection + if (isMultiselection) { + properties["number_of_media_selected"] = identifiers.size + } + analyticsTrackerWrapper.track(MEDIA_PICKER_RECENT_MEDIA_SELECTED, properties) + } + } + + class DeviceListInsertUseCaseFactory + @Inject constructor( + private val wpMediaUtilsWrapper: WPMediaUtilsWrapper, + private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, + private val analyticsUtilsWrapper: AnalyticsUtilsWrapper + ) { + fun build(queueResults: Boolean): DeviceListInsertUseCase { + return DeviceListInsertUseCase( + wpMediaUtilsWrapper, + analyticsTrackerWrapper, + analyticsUtilsWrapper, + queueResults + ) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/MediaInsertHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/MediaInsertHandler.kt new file mode 100644 index 000000000000..a6a51b03803f --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/MediaInsertHandler.kt @@ -0,0 +1,16 @@ +package org.wordpress.android.ui.mediapicker.insert + +import kotlinx.coroutines.flow.Flow +import org.wordpress.android.ui.mediapicker.MediaItem.Identifier + +class MediaInsertHandler(private val mediaInsertUseCase: MediaInsertUseCase) { + suspend fun insertMedia(identifiers: List): Flow { + return mediaInsertUseCase.insert(identifiers) + } + + sealed class InsertModel { + data class Success(val identifiers: List) : InsertModel() + data class Error(val error: String) : InsertModel() + object Progress : InsertModel() + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/MediaInsertHandlerFactory.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/MediaInsertHandlerFactory.kt new file mode 100644 index 000000000000..32ff8d043635 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/MediaInsertHandlerFactory.kt @@ -0,0 +1,32 @@ +package org.wordpress.android.ui.mediapicker.insert + +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.mediapicker.MediaPickerSetup +import org.wordpress.android.ui.mediapicker.insert.DeviceListInsertUseCase.DeviceListInsertUseCaseFactory +import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.DEVICE +import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.GIF_LIBRARY +import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.STOCK_LIBRARY +import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.WP_LIBRARY +import org.wordpress.android.ui.mediapicker.insert.StockMediaInsertUseCase.StockMediaInsertUseCaseFactory +import javax.inject.Inject + +class MediaInsertHandlerFactory +@Inject constructor( + private val deviceListInsertUseCaseFactory: DeviceListInsertUseCaseFactory, + private val stockMediaInsertUseCaseFactory: StockMediaInsertUseCaseFactory +) { + fun build(mediaPickerSetup: MediaPickerSetup, siteModel: SiteModel?): MediaInsertHandler { + return when (mediaPickerSetup.dataSource) { + DEVICE -> deviceListInsertUseCaseFactory.build(mediaPickerSetup.queueResults) + WP_LIBRARY -> DefaultMediaInsertUseCase + STOCK_LIBRARY -> stockMediaInsertUseCaseFactory.build(requireNotNull(siteModel) { + "Site is necessary when inserting into stock media library " + }) + GIF_LIBRARY -> DefaultMediaInsertUseCase + }.toMediaInsertHandler() + } + + private fun MediaInsertUseCase.toMediaInsertHandler() = MediaInsertHandler(this) + + private object DefaultMediaInsertUseCase : MediaInsertUseCase +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/MediaInsertUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/MediaInsertUseCase.kt new file mode 100644 index 000000000000..4a3599ce8daf --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/MediaInsertUseCase.kt @@ -0,0 +1,11 @@ +package org.wordpress.android.ui.mediapicker.insert + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import org.wordpress.android.ui.mediapicker.MediaItem.Identifier +import org.wordpress.android.ui.mediapicker.insert.MediaInsertHandler.InsertModel +import org.wordpress.android.ui.mediapicker.insert.MediaInsertHandler.InsertModel.Success + +interface MediaInsertUseCase { + suspend fun insert(identifiers: List): Flow = flowOf(Success(identifiers)) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/StockMediaInsertUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/StockMediaInsertUseCase.kt new file mode 100644 index 000000000000..fd232c379e6f --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/insert/StockMediaInsertUseCase.kt @@ -0,0 +1,58 @@ +package org.wordpress.android.ui.mediapicker.insert + +import kotlinx.coroutines.flow.flow +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.analytics.AnalyticsTracker.Stat.STOCK_MEDIA_UPLOADED +import org.wordpress.android.fluxc.model.MediaModel +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.StockMediaStore +import org.wordpress.android.fluxc.store.StockMediaUploadItem +import org.wordpress.android.ui.mediapicker.MediaItem.Identifier +import org.wordpress.android.ui.mediapicker.MediaItem.Identifier.StockMediaIdentifier +import org.wordpress.android.ui.mediapicker.insert.MediaInsertHandler.InsertModel +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.AppLog.T.MEDIA +import java.util.HashMap +import javax.inject.Inject + +class StockMediaInsertUseCase( + private val site: SiteModel, + private val stockMediaStore: StockMediaStore +) : MediaInsertUseCase { + override suspend fun insert(identifiers: List) = flow { + emit(InsertModel.Progress) + val result = stockMediaStore.performUploadStockMedia(site, identifiers.mapNotNull { identifier -> + (identifier as? StockMediaIdentifier)?.let { + StockMediaUploadItem(it.name, it.title, it.url) + } + }) + emit(when { + result.error != null -> InsertModel.Error(result.error.message) + else -> { + trackUploadedStockMediaEvent(result.mediaList) + InsertModel.Success(result.mediaList.mapNotNull { Identifier.RemoteId(it.mediaId) }) + } + }) + } + + private fun trackUploadedStockMediaEvent(mediaList: List) { + if (mediaList.isEmpty()) { + AppLog.e(MEDIA, "Cannot track uploaded stock media event if mediaList is empty") + return + } + val isMultiselect = mediaList.size > 1 + val properties: MutableMap = HashMap() + properties["is_part_of_multiselection"] = isMultiselect + properties["number_of_media_selected"] = mediaList.size + AnalyticsTracker.track(STOCK_MEDIA_UPLOADED, properties) + } + + class StockMediaInsertUseCaseFactory + @Inject constructor( + private val stockMediaStore: StockMediaStore + ) { + fun build(site: SiteModel): StockMediaInsertUseCase { + return StockMediaInsertUseCase(site, stockMediaStore) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/DeviceListBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/DeviceListBuilder.kt similarity index 77% rename from WordPress/src/main/java/org/wordpress/android/ui/mediapicker/DeviceListBuilder.kt rename to WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/DeviceListBuilder.kt index c86bdea4350e..d411a252e131 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/DeviceListBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/DeviceListBuilder.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.mediapicker +package org.wordpress.android.ui.mediapicker.loader import android.content.ContentResolver import android.content.Context @@ -16,8 +16,10 @@ import kotlinx.coroutines.async import kotlinx.coroutines.withContext import org.wordpress.android.fluxc.utils.MimeTypes import org.wordpress.android.modules.BG_THREAD -import org.wordpress.android.ui.mediapicker.MediaItem.Identifier -import org.wordpress.android.ui.mediapicker.MediaSource.MediaLoadingResult +import org.wordpress.android.ui.mediapicker.MediaItem +import org.wordpress.android.ui.mediapicker.MediaItem.Identifier.LocalUri +import org.wordpress.android.ui.mediapicker.MediaType +import org.wordpress.android.ui.mediapicker.loader.MediaSource.MediaLoadingResult import org.wordpress.android.ui.mediapicker.MediaType.AUDIO import org.wordpress.android.ui.mediapicker.MediaType.DOCUMENT import org.wordpress.android.ui.mediapicker.MediaType.IMAGE @@ -30,19 +32,18 @@ import org.wordpress.android.util.UriWrapper import javax.inject.Inject import javax.inject.Named -class DeviceListBuilder -@Inject constructor( - val context: Context, +class DeviceListBuilder( + private val context: Context, private val localeManagerWrapper: LocaleManagerWrapper, - @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher + @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, + private val mediaTypes: Set ) : MediaSource { private val mimeTypes = MimeTypes() - private val cachedData = mutableListOf() override suspend fun load( - mediaTypes: Set, forced: Boolean, - loadMore: Boolean + loadMore: Boolean, + filter: String? ): MediaLoadingResult { return withContext(bgDispatcher) { val result = mutableListOf() @@ -56,21 +57,14 @@ class DeviceListBuilder } deferredJobs.forEach { result.addAll(it.await()) } result.sortByDescending { it.dataModified } - cachedData.clear() - cachedData.addAll(result) - MediaLoadingResult.Success(false) - } - } - - override suspend fun get(mediaTypes: Set, filter: String?): List { - return if (filter == null) { - cachedData - } else { - val lowerCaseFilter = filter.toLowerCase(localeManagerWrapper.getLocale()) - cachedData.filter { - it.name?.toLowerCase(localeManagerWrapper.getLocale()) - ?.contains(lowerCaseFilter) == true - } + val lowerCaseFilter = filter?.toLowerCase(localeManagerWrapper.getLocale()) + val filteredResult = lowerCaseFilter?.let { + result.filter { + it.name?.toLowerCase(localeManagerWrapper.getLocale()) + ?.contains(lowerCaseFilter) == true + } + } ?: result + MediaLoadingResult.Success(filteredResult, false) } } @@ -102,7 +96,7 @@ class DeviceListBuilder val title = cursor.getString(titleIndex) val uri = Uri.withAppendedPath(baseUri, "" + id) val item = MediaItem( - Identifier.LocalUri(UriWrapper(uri)), + LocalUri(UriWrapper(uri)), uri.toString(), title, mediaType, @@ -125,7 +119,7 @@ class DeviceListBuilder val mimeType = getMimeType(uri) if (mimeType != null && mimeTypes.isSupportedApplicationType(mimeType)) { MediaItem( - Identifier.LocalUri(UriWrapper(uri)), + LocalUri(UriWrapper(uri)), uri.toString(), file.name, DOCUMENT, @@ -152,4 +146,20 @@ class DeviceListBuilder private const val ID_DATE_MODIFIED = MediaColumns.DATE_MODIFIED private const val ID_TITLE = MediaColumns.TITLE } + + class DeviceListBuilderFactory + @Inject constructor( + private val context: Context, + private val localeManagerWrapper: LocaleManagerWrapper, + @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher + ) { + fun build(mediaTypes: Set): DeviceListBuilder { + return DeviceListBuilder( + context, + localeManagerWrapper, + bgDispatcher, + mediaTypes + ) + } + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaLibraryDataSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/MediaLibraryDataSource.kt similarity index 88% rename from WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaLibraryDataSource.kt rename to WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/MediaLibraryDataSource.kt index d1e2bd15d7e6..89abbbac4f7a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaLibraryDataSource.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/MediaLibraryDataSource.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.mediapicker +package org.wordpress.android.ui.mediapicker.loader import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.async @@ -14,8 +14,10 @@ import org.wordpress.android.fluxc.store.MediaStore.FetchMediaListPayload import org.wordpress.android.fluxc.store.MediaStore.OnMediaListFetched import org.wordpress.android.fluxc.utils.MimeType import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.ui.mediapicker.MediaItem import org.wordpress.android.ui.mediapicker.MediaItem.Identifier.RemoteId -import org.wordpress.android.ui.mediapicker.MediaSource.MediaLoadingResult +import org.wordpress.android.ui.mediapicker.MediaType +import org.wordpress.android.ui.mediapicker.loader.MediaSource.MediaLoadingResult import org.wordpress.android.ui.mediapicker.MediaType.AUDIO import org.wordpress.android.ui.mediapicker.MediaType.DOCUMENT import org.wordpress.android.ui.mediapicker.MediaType.IMAGE @@ -30,7 +32,8 @@ class MediaLibraryDataSource( private val mediaStore: MediaStore, private val dispatcher: Dispatcher, @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, - private val siteModel: SiteModel + private val siteModel: SiteModel, + private val mediaTypes: Set ) : MediaSource { init { dispatcher.register(this) @@ -39,9 +42,9 @@ class MediaLibraryDataSource( private var loadContinuations = mutableMapOf>() override suspend fun load( - mediaTypes: Set, forced: Boolean, - loadMore: Boolean + loadMore: Boolean, + filter: String? ): MediaLoadingResult { return withContext(bgDispatcher) { val loadingResults = mediaTypes.map { mediaType -> @@ -67,12 +70,12 @@ class MediaLibraryDataSource( if (error != null) { MediaLoadingResult.Failure(error) } else { - MediaLoadingResult.Success(hasMore) + MediaLoadingResult.Success(get(mediaTypes, filter), hasMore) } } } - override suspend fun get(mediaTypes: Set, filter: String?): List { + private suspend fun get(mediaTypes: Set, filter: String?): List { return withContext(bgDispatcher) { mediaTypes.map { mediaType -> async { @@ -157,6 +160,7 @@ class MediaLibraryDataSource( private val dispatcher: Dispatcher, @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher ) { - fun build(siteModel: SiteModel) = MediaLibraryDataSource(mediaStore, dispatcher, bgDispatcher, siteModel) + fun build(siteModel: SiteModel, mediaTypes: Set) = + MediaLibraryDataSource(mediaStore, dispatcher, bgDispatcher, siteModel, mediaTypes) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/MediaLoader.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/MediaLoader.kt new file mode 100644 index 000000000000..f2b31e53dce7 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/MediaLoader.kt @@ -0,0 +1,112 @@ +package org.wordpress.android.ui.mediapicker.loader + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.flow +import org.wordpress.android.ui.mediapicker.MediaItem +import org.wordpress.android.ui.mediapicker.loader.MediaLoader.LoadAction.ClearFilter +import org.wordpress.android.ui.mediapicker.loader.MediaLoader.LoadAction.Filter +import org.wordpress.android.ui.mediapicker.loader.MediaLoader.LoadAction.NextPage +import org.wordpress.android.ui.mediapicker.loader.MediaLoader.LoadAction.Refresh +import org.wordpress.android.ui.mediapicker.loader.MediaLoader.LoadAction.Start +import org.wordpress.android.ui.mediapicker.loader.MediaSource.MediaLoadingResult +import org.wordpress.android.ui.mediapicker.loader.MediaSource.MediaLoadingResult.Failure +import org.wordpress.android.ui.mediapicker.loader.MediaSource.MediaLoadingResult.Success +import org.wordpress.android.util.LocaleManagerWrapper + +data class MediaLoader( + private val mediaSource: MediaSource, + private val localeManagerWrapper: LocaleManagerWrapper +) { + suspend fun loadMedia(actions: Channel): Flow { + return flow { + var state = DomainModel() + for (loadAction in actions) { + when (loadAction) { + is Start -> { + if (state.domainItems.isEmpty() || state.error != null) { + state = updateState( + buildDomainModel(mediaSource.load(filter = state.filter), state) + ) + } + } + is Refresh -> { + if (loadAction.forced || state.domainItems.isEmpty()) { + state = updateState(state.copy(isLoading = true)) + state = updateState( + buildDomainModel( + mediaSource.load( + forced = loadAction.forced, + filter = state.filter + ), state + ) + ) + } + } + is NextPage -> { + val load = mediaSource.load(loadMore = true, filter = state.filter) + state = updateState(buildDomainModel(load, state)) + } + is Filter -> { + if (loadAction.filter != state.filter) { + state = updateState(state.copy(filter = loadAction.filter, isLoading = true)) + val load = mediaSource.load( + filter = state.filter + ) + state = updateState(buildDomainModel(load, state)) + } + } + is ClearFilter -> { + if (!state.filter.isNullOrEmpty()) { + state = updateState(state.copy(filter = null, isLoading = true)) + val load = mediaSource.load( + filter = state.filter + ) + state = updateState(buildDomainModel(load, state)) + } + } + } + } + } + } + + private suspend fun FlowCollector.updateState( + updatedState: DomainModel + ): DomainModel { + emit(updatedState) + return updatedState + } + + private fun buildDomainModel( + partialResult: MediaLoadingResult, + state: DomainModel + ): DomainModel { + return when (partialResult) { + is Success -> state.copy( + isLoading = false, + error = null, + hasMore = partialResult.hasMore, + domainItems = partialResult.data + ) + is Failure -> state.copy(isLoading = false, error = partialResult.message) + } + } + + sealed class LoadAction { + data class Start(val filter: String? = null) : LoadAction() + data class Refresh(val forced: Boolean) : LoadAction() + data class Filter(val filter: String) : LoadAction() + object NextPage : LoadAction() + object ClearFilter : LoadAction() + } + + data class DomainModel( + val domainItems: List = listOf(), + val error: String? = null, + val hasMore: Boolean = false, + val isFilteredResult: Boolean = false, + val filter: String? = null, + val isLoading: Boolean = false + ) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaLoaderFactory.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/MediaLoaderFactory.kt similarity index 58% rename from WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaLoaderFactory.kt rename to WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/MediaLoaderFactory.kt index 04061870f44b..07f1420ef4de 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaLoaderFactory.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/MediaLoaderFactory.kt @@ -1,31 +1,33 @@ -package org.wordpress.android.ui.mediapicker +package org.wordpress.android.ui.mediapicker.loader import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.ui.mediapicker.MediaLibraryDataSource.MediaLibraryDataSourceFactory +import org.wordpress.android.ui.mediapicker.MediaPickerSetup import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.DEVICE import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.GIF_LIBRARY import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.STOCK_LIBRARY import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.WP_LIBRARY +import org.wordpress.android.ui.mediapicker.loader.DeviceListBuilder.DeviceListBuilderFactory +import org.wordpress.android.ui.mediapicker.loader.MediaLibraryDataSource.MediaLibraryDataSourceFactory import org.wordpress.android.util.LocaleManagerWrapper import javax.inject.Inject class MediaLoaderFactory @Inject constructor( - private val deviceListBuilder: DeviceListBuilder, + private val deviceListBuilderFactory: DeviceListBuilderFactory, private val mediaLibraryDataSourceFactory: MediaLibraryDataSourceFactory, + private val stockMediaDataSource: StockMediaDataSource, private val localeManagerWrapper: LocaleManagerWrapper ) { fun build(mediaPickerSetup: MediaPickerSetup, siteModel: SiteModel?): MediaLoader { return when (mediaPickerSetup.dataSource) { - DEVICE -> deviceListBuilder + DEVICE -> deviceListBuilderFactory.build(mediaPickerSetup.allowedTypes) WP_LIBRARY -> mediaLibraryDataSourceFactory.build(requireNotNull(siteModel) { "Site is necessary when loading WP media library " - }) - STOCK_LIBRARY -> throw NotImplementedError("Source not implemented yet") + }, mediaPickerSetup.allowedTypes) + STOCK_LIBRARY -> stockMediaDataSource GIF_LIBRARY -> throw NotImplementedError("Source not implemented yet") - }.toMediaLoader(mediaPickerSetup) + }.toMediaLoader() } - private fun MediaSource.toMediaLoader(mediaPickerSetup: MediaPickerSetup) = - MediaLoader(this, localeManagerWrapper, mediaPickerSetup.allowedTypes) + private fun MediaSource.toMediaLoader() = MediaLoader(this, localeManagerWrapper) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/MediaSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/MediaSource.kt new file mode 100644 index 000000000000..d193847fb01b --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/MediaSource.kt @@ -0,0 +1,16 @@ +package org.wordpress.android.ui.mediapicker.loader + +import org.wordpress.android.ui.mediapicker.MediaItem + +interface MediaSource { + suspend fun load( + forced: Boolean = false, + loadMore: Boolean = false, + filter: String? = null + ): MediaLoadingResult + + sealed class MediaLoadingResult { + data class Success(val data: List, val hasMore: Boolean = false) : MediaLoadingResult() + data class Failure(val message: String) : MediaLoadingResult() + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/StockMediaDataSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/StockMediaDataSource.kt new file mode 100644 index 000000000000..06cb38445008 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/StockMediaDataSource.kt @@ -0,0 +1,67 @@ +package org.wordpress.android.ui.mediapicker.loader + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import org.wordpress.android.fluxc.store.StockMediaStore +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.ui.mediapicker.MediaItem +import org.wordpress.android.ui.mediapicker.MediaItem.Identifier.StockMediaIdentifier +import org.wordpress.android.ui.mediapicker.MediaType.IMAGE +import org.wordpress.android.ui.mediapicker.loader.MediaSource.MediaLoadingResult +import org.wordpress.android.ui.mediapicker.loader.MediaSource.MediaLoadingResult.Failure +import org.wordpress.android.ui.mediapicker.loader.MediaSource.MediaLoadingResult.Success +import javax.inject.Inject +import javax.inject.Named + +class StockMediaDataSource +@Inject constructor( + private val stockMediaStore: StockMediaStore, + @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher +) : MediaSource { + override suspend fun load( + forced: Boolean, + loadMore: Boolean, + filter: String? + ): MediaLoadingResult { + return withValidFilter(filter) { validFilter -> + val result = stockMediaStore.fetchStockMedia(validFilter, loadMore) + val error = result.error + return@withValidFilter when { + error != null -> { + Failure(error.message) + } + else -> Success(get(), result.canLoadMore) + } + } ?: Success(listOf(), false) + } + + private suspend fun get(): List { + return stockMediaStore.getStockMedia() + .mapNotNull { + it.url?.let { url -> + MediaItem( + StockMediaIdentifier(it.url, it.name, it.title), + url, + it.name, + IMAGE, + null, + it.date?.toLongOrNull() ?: 0 + ) + } + } + } + + private suspend fun withValidFilter(filter: String?, action: suspend (filter: String) -> T): T? { + return if (filter != null && filter.length >= MIN_SEARCH_QUERY_SIZE) { + withContext(bgDispatcher) { + action(filter) + } + } else { + null + } + } + + companion object { + private const val MIN_SEARCH_QUERY_SIZE = 3 + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/DeviceMediaListBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/DeviceMediaListBuilder.kt index dfa34f513854..135686f758bc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/DeviceMediaListBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/DeviceMediaListBuilder.kt @@ -17,7 +17,7 @@ import javax.inject.Inject import javax.inject.Named @Deprecated("This class is being refactored, if you implement any change, please also update " + - "{@link org.wordpress.android.ui.mediapicker.DeviceListBuilder}") + "{@link org.wordpress.android.ui.mediapicker.loader.DeviceListBuilder}") class DeviceMediaListBuilder @Inject constructor( val context: Context, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/MediaPickerLauncher.kt b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/MediaPickerLauncher.kt index a91c83dddbd4..9e64a8d4adb4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/MediaPickerLauncher.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/MediaPickerLauncher.kt @@ -14,6 +14,7 @@ import org.wordpress.android.ui.media.MediaBrowserType.GRAVATAR_IMAGE_PICKER import org.wordpress.android.ui.mediapicker.MediaPickerActivity import org.wordpress.android.ui.mediapicker.MediaPickerSetup import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.DEVICE +import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.STOCK_LIBRARY import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.WP_LIBRARY import org.wordpress.android.ui.mediapicker.MediaType import org.wordpress.android.ui.mediapicker.MediaType.AUDIO @@ -107,6 +108,7 @@ class MediaPickerLauncher systemPickerEnabled = true, editingEnabled = true, queueResults = false, + defaultSearchView = false, title = R.string.photo_picker_choose_file ) val intent = MediaPickerActivity.buildIntent( @@ -140,6 +142,36 @@ class MediaPickerLauncher } } + fun showStockMediaPickerForResult( + activity: Activity, + site: SiteModel, + requestCode: Int, + allowMultipleSelection: Boolean + ) { + if (consolidatedMediaPickerFeatureConfig.isEnabled()) { + val mediaPickerSetup = MediaPickerSetup( + dataSource = STOCK_LIBRARY, + canMultiselect = allowMultipleSelection, + requiresStoragePermissions = false, + allowedTypes = setOf(IMAGE), + cameraEnabled = false, + systemPickerEnabled = false, + editingEnabled = false, + queueResults = false, + defaultSearchView = true, + title = R.string.photo_picker_stock_media + ) + val intent = MediaPickerActivity.buildIntent( + activity, + mediaPickerSetup, + site + ) + activity.startActivityForResult(intent, requestCode) + } else { + ActivityLauncher.showStockMediaPickerForResult(activity, site, requestCode) + } + } + private fun buildLocalMediaPickerSetup(browserType: MediaBrowserType): MediaPickerSetup { val allowedTypes = mutableSetOf() if (browserType.isImagePicker) { @@ -164,6 +196,7 @@ class MediaPickerLauncher systemPickerEnabled = true, editingEnabled = browserType.isImagePicker, queueResults = browserType == FEATURED_IMAGE_PICKER, + defaultSearchView = false, title = title ) } @@ -185,6 +218,7 @@ class MediaPickerLauncher systemPickerEnabled = false, editingEnabled = false, queueResults = false, + defaultSearchView = false, title = R.string.wp_media_title ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PermissionsHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PermissionsHandler.kt index ab193f1e45d8..01f6280b077a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PermissionsHandler.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PermissionsHandler.kt @@ -13,12 +13,22 @@ class PermissionsHandler } fun hasStoragePermission(): Boolean { + return hasReadStoragePermission() && hasWriteStoragePermission() + } + + fun hasWriteStoragePermission(): Boolean { return ContextCompat.checkSelfPermission( context, permission.WRITE_EXTERNAL_STORAGE ) == PackageManager.PERMISSION_GRANTED } - fun hasCameraPermission(): Boolean { + private fun hasReadStoragePermission(): Boolean { + return ContextCompat.checkSelfPermission( + context, permission.READ_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED + } + + private fun hasCameraPermission(): Boolean { return ContextCompat.checkSelfPermission( context, permission.CAMERA diff --git a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModel.kt index d3fc8eea89cb..467690fe5906 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModel.kt @@ -220,7 +220,7 @@ class PhotoPickerViewModel @Inject constructor( } fun refreshData(forceReload: Boolean) { - if (!permissionsHandler.hasStoragePermission()) { + if (!permissionsHandler.hasWriteStoragePermission()) { return } launch(bgDispatcher) { @@ -426,7 +426,7 @@ class PhotoPickerViewModel @Inject constructor( } fun checkStoragePermission(isAlwaysDenied: Boolean) { - if (permissionsHandler.hasStoragePermission()) { + if (permissionsHandler.hasWriteStoragePermission()) { _softAskRequest.value = SoftAskRequest(show = false, isAlwaysDenied = isAlwaysDenied) if (_photoPickerItems.value.isNullOrEmpty()) { refreshData(false) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java index 4eca4678c2ee..43ccddead92e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java @@ -19,6 +19,7 @@ import android.view.View; import android.view.ViewGroup; import android.webkit.MimeTypeMap; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -131,6 +132,7 @@ import org.wordpress.android.ui.posts.EditPostRepository.UpdatePostResult; import org.wordpress.android.ui.posts.EditPostRepository.UpdatePostResult.Updated; import org.wordpress.android.ui.posts.EditPostSettingsFragment.EditPostSettingsCallback; +import org.wordpress.android.ui.posts.FeaturedImageHelper.EnqueueFeaturedImageResult; import org.wordpress.android.ui.posts.InsertMediaDialog.InsertMediaCallback; import org.wordpress.android.ui.posts.PostEditorAnalyticsSession.Editor; import org.wordpress.android.ui.posts.PostEditorAnalyticsSession.Outcome; @@ -194,6 +196,7 @@ import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper; import org.wordpress.android.util.analytics.AnalyticsUtils; import org.wordpress.android.util.analytics.AnalyticsUtils.BlockEditorEnabledSource; +import org.wordpress.android.util.config.ConsolidatedMediaPickerFeatureConfig; import org.wordpress.android.util.config.GutenbergMentionsFeatureConfig; import org.wordpress.android.util.config.ModalLayoutPickerFeatureConfig; import org.wordpress.android.util.config.TenorFeatureConfig; @@ -382,6 +385,7 @@ enum RestartEditorOptions { @Inject TenorFeatureConfig mTenorFeatureConfig; @Inject GutenbergMentionsFeatureConfig mGutenbergMentionsFeatureConfig; @Inject ModalLayoutPickerFeatureConfig mModalLayoutPickerFeatureConfig; + @Inject ConsolidatedMediaPickerFeatureConfig mConsolidatedMediaPickerFeatureConfig; @Inject CrashLogging mCrashLogging; @Inject MediaPickerLauncher mMediaPickerLauncher; @@ -1069,7 +1073,8 @@ public void onPhotoPickerIconClicked(@NonNull PhotoPickerIcon icon, boolean allo final int requestCode = allowMultipleSelection ? RequestCodes.STOCK_MEDIA_PICKER_MULTI_SELECT : RequestCodes.STOCK_MEDIA_PICKER_SINGLE_SELECT_FOR_GUTENBERG_BLOCK; - ActivityLauncher.showStockMediaPickerForResult(this, mSite, requestCode); + mMediaPickerLauncher + .showStockMediaPickerForResult(this, mSite, requestCode, allowMultipleSelection); break; case GIF: ActivityLauncher.showGifPickerForResult(this, mSite, RequestCodes.GIF_PICKER_SINGLE_SELECT); @@ -2455,6 +2460,34 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { long mediaId = data.getLongExtra(MediaPickerConstants.EXTRA_MEDIA_ID, 0); setFeaturedImageId(mediaId, true); } else if (data.hasExtra(MediaPickerConstants.EXTRA_MEDIA_QUEUED)) { + if (mConsolidatedMediaPickerFeatureConfig.isEnabled()) { + List uris = convertStringArrayIntoUrisList( + data.getStringArrayExtra(MediaPickerConstants.EXTRA_MEDIA_URIS)); + int postId = getImmutablePost().getId(); + mFeaturedImageHelper.trackFeaturedImageEvent( + FeaturedImageHelper.TrackableEvent.IMAGE_PICKED, + postId + ); + for (Uri mediaUri : uris) { + String mimeType = getContentResolver().getType(mediaUri); + EnqueueFeaturedImageResult queueImageResult = mFeaturedImageHelper + .queueFeaturedImageForUpload( + postId, getSite(), mediaUri, + mimeType + ); + if (queueImageResult == EnqueueFeaturedImageResult.FILE_NOT_FOUND) { + Toast.makeText( + this, + R.string.file_not_found, Toast.LENGTH_SHORT + ).show(); + } else if (queueImageResult == EnqueueFeaturedImageResult.INVALID_POST_ID) { + Toast.makeText( + this, + R.string.error_generic, Toast.LENGTH_SHORT + ).show(); + } + } + } if (mEditPostSettingsFragment != null) { mEditPostSettingsFragment.refreshViews(); } @@ -2497,7 +2530,11 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { break; case RequestCodes.STOCK_MEDIA_PICKER_MULTI_SELECT: if (data.hasExtra(StockMediaPickerActivity.KEY_UPLOADED_MEDIA_IDS)) { - long[] mediaIds = data.getLongArrayExtra(StockMediaPickerActivity.KEY_UPLOADED_MEDIA_IDS); + String key = StockMediaPickerActivity.KEY_UPLOADED_MEDIA_IDS; + if (mConsolidatedMediaPickerFeatureConfig.isEnabled()) { + key = MediaBrowserActivity.RESULT_IDS; + } + long[] mediaIds = data.getLongArrayExtra(key); mEditorMedia .addExistingMediaToEditorAsync(AddExistingMediaSource.STOCK_PHOTO_LIBRARY, mediaIds); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderCardUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderCardUiState.kt index 693bec24df44..cc27d9402ac7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderCardUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderCardUiState.kt @@ -20,7 +20,7 @@ sealed class ReaderCardUiState { val blogId: Long, val dateLine: String, val title: UiString?, - val blogName: String?, + val blogName: UiString, val excerpt: String?, // mTxtText val blogUrl: String?, val tagItems: List, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderPostUiStateBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderPostUiStateBuilder.kt index 046ade52a2d9..ca6d0f5707be 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderPostUiStateBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderPostUiStateBuilder.kt @@ -240,7 +240,8 @@ class ReaderPostUiStateBuilder @Inject constructor( private fun buildExcerpt(post: ReaderPost) = post.takeIf { post.cardType != PHOTO && post.hasExcerpt() }?.excerpt - private fun buildBlogName(post: ReaderPost) = post.takeIf { it.hasBlogName() }?.blogName + private fun buildBlogName(post: ReaderPost) = post.takeIf { it.hasBlogName() }?.blogName?.let { UiStringText(it) } + ?: UiStringRes(R.string.untitled_in_parentheses) private fun buildAvatarOrBlavatarUrl(post: ReaderPost) = post.takeIf { it.hasBlogImageUrl() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverLogic.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverLogic.kt index e971a2a8f3eb..ea087337e5c0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverLogic.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverLogic.kt @@ -214,7 +214,7 @@ class ReaderDiscoverLogic( } private fun clearCache() { - ReaderDiscoverCardsTable.reset() + ReaderDiscoverCardsTable.clear() ReaderPostTable.deletePostsWithTag(ReaderTag.createDiscoverPostCardsTag()) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stories/StoriesAnalyticsReceiver.kt b/WordPress/src/main/java/org/wordpress/android/ui/stories/StoriesAnalyticsReceiver.kt new file mode 100644 index 000000000000..1b64d844f8cd --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/stories/StoriesAnalyticsReceiver.kt @@ -0,0 +1,14 @@ +package org.wordpress.android.ui.stories + +import com.wordpress.stories.compose.StoriesAnalyticsListener +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.analytics.AnalyticsTracker.Stat + +/** + * Receives tracker-agnostic analytics events from the Stories library and forwards them to [AnalyticsTracker]. + */ +class StoriesAnalyticsReceiver : StoriesAnalyticsListener { + override fun trackStoryTextChanged(properties: Map) { + AnalyticsTracker.track(Stat.STORY_TEXT_CHANGED, properties) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stories/StoryComposerActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/stories/StoryComposerActivity.kt index 080e1bae3514..f539b1c1b0b4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stories/StoryComposerActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stories/StoryComposerActivity.kt @@ -112,6 +112,7 @@ class StoryComposerActivity : ComposeLoopFrameActivity(), setNotificationExtrasLoader(this) setMetadataProvider(this) setStoryDiscardListener(this) + setStoriesAnalyticsListener(StoriesAnalyticsReceiver()) setNotificationTrackerProvider((application as WordPress).getStoryNotificationTrackerProvider()) setPrepublishingEventProvider(this) setPermissionDialogProvider(this) diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPMediaUtilsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/util/WPMediaUtilsWrapper.kt new file mode 100644 index 000000000000..27d502425bef --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/WPMediaUtilsWrapper.kt @@ -0,0 +1,12 @@ +package org.wordpress.android.util + +import android.content.Context +import android.net.Uri +import javax.inject.Inject + +class WPMediaUtilsWrapper +@Inject constructor(private val context: Context) { + fun fetchMedia(mediaUri: Uri): Uri? { + return WPMediaUtils.fetchMedia(context, mediaUri) + } +} diff --git a/WordPress/src/main/res/layout/media_picker_fragment.xml b/WordPress/src/main/res/layout/media_picker_fragment.xml index 93ea12366f3b..8978c9e35d5b 100644 --- a/WordPress/src/main/res/layout/media_picker_fragment.xml +++ b/WordPress/src/main/res/layout/media_picker_fragment.xml @@ -4,6 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:id="@+id/coordinator" android:orientation="vertical"> + + + + diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 7c16ca17629d..247227304218 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -162,6 +162,8 @@ Accessing content of a private site Failed to access content of a private site. Some media might be unavailable + Uploading stock media + Queued Uploading Deleting @@ -2349,6 +2351,9 @@ Photo library Search to find free photos to add to your Media Library Photos provided by %s + Media insert failed: %s + Media insert failed. + %1$s was denied access to your photos. To fix this, edit your permissions and turn on %2$s and %3$s. Search Tenor diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModelTest.kt index 4460c4bce93f..bca66459d5d0 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModelTest.kt @@ -20,7 +20,6 @@ import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.test import org.wordpress.android.ui.mediapicker.MediaItem.Identifier import org.wordpress.android.ui.mediapicker.MediaItem.Identifier.LocalUri -import org.wordpress.android.ui.mediapicker.MediaLoader.DomainModel import org.wordpress.android.ui.mediapicker.MediaPickerFragment.ChooserContext import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerAction.OpenSystemPicker import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.DEVICE @@ -30,7 +29,6 @@ import org.wordpress.android.ui.mediapicker.MediaPickerUiItem.PhotoItem import org.wordpress.android.ui.mediapicker.MediaPickerUiItem.VideoItem import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.ActionModeUiModel import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.EditActionUiModel -import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.FabUiModel import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.IconClickEvent import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.MediaPickerUiState import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.PhotoListUiModel.Data @@ -42,6 +40,11 @@ import org.wordpress.android.ui.mediapicker.MediaType.AUDIO import org.wordpress.android.ui.mediapicker.MediaType.DOCUMENT import org.wordpress.android.ui.mediapicker.MediaType.IMAGE import org.wordpress.android.ui.mediapicker.MediaType.VIDEO +import org.wordpress.android.ui.mediapicker.insert.MediaInsertHandler +import org.wordpress.android.ui.mediapicker.insert.MediaInsertHandlerFactory +import org.wordpress.android.ui.mediapicker.loader.MediaLoader +import org.wordpress.android.ui.mediapicker.loader.MediaLoader.DomainModel +import org.wordpress.android.ui.mediapicker.loader.MediaLoaderFactory import org.wordpress.android.ui.photopicker.PermissionsHandler import org.wordpress.android.ui.utils.UiString import org.wordpress.android.ui.utils.UiString.UiStringRes @@ -58,6 +61,8 @@ import java.util.Locale class MediaPickerViewModelTest : BaseUnitTest() { @Mock lateinit var mediaLoaderFactory: MediaLoaderFactory @Mock lateinit var mediaLoader: MediaLoader + @Mock lateinit var mediaInsertHandlerFactory: MediaInsertHandlerFactory + @Mock lateinit var mediaInsertHandler: MediaInsertHandler @Mock lateinit var analyticsUtilsWrapper: AnalyticsUtilsWrapper @Mock lateinit var analyticsTrackerWrapper: AnalyticsTrackerWrapper @Mock lateinit var uriWrapper1: UriWrapper @@ -88,6 +93,7 @@ class MediaPickerViewModelTest : BaseUnitTest() { TEST_DISPATCHER, TEST_DISPATCHER, mediaLoaderFactory, + mediaInsertHandlerFactory, analyticsUtilsWrapper, analyticsTrackerWrapper, permissionsHandler, @@ -633,6 +639,7 @@ class MediaPickerViewModelTest : BaseUnitTest() { ) } }) + whenever(mediaInsertHandlerFactory.build(mediaPickerSetup, site)).thenReturn(mediaInsertHandler) viewModel.start(listOf(), mediaPickerSetup, null, site) viewModel.uiState.observeForever { @@ -723,20 +730,19 @@ class MediaPickerViewModelTest : BaseUnitTest() { systemPickerEnabled = true, editingEnabled = editingEnabled, queueResults = false, + defaultSearchView = false, title = R.string.wp_media_title ) private fun assertStoriesFabIsVisible() { uiStates.last().fabUiModel.let { model -> - assertThat(model is FabUiModel).isTrue() - assertThat((model as FabUiModel).show).isEqualTo(true) + assertThat(model.show).isEqualTo(true) } } private fun assertStoriesFabIsHidden() { uiStates.last().fabUiModel.let { model -> - assertThat(model is FabUiModel).isTrue() - assertThat((model as FabUiModel).show).isEqualTo(false) + assertThat(model.show).isEqualTo(false) } } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/insert/StockMediaInsertUseCaseTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/insert/StockMediaInsertUseCaseTest.kt new file mode 100644 index 000000000000..2b86d101dc87 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/insert/StockMediaInsertUseCaseTest.kt @@ -0,0 +1,65 @@ +package org.wordpress.android.ui.mediapicker.insert + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.flow.toList +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.fluxc.model.MediaModel +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.MediaStore.OnStockMediaUploaded +import org.wordpress.android.fluxc.store.StockMediaItem +import org.wordpress.android.fluxc.store.StockMediaStore +import org.wordpress.android.fluxc.store.StockMediaUploadItem +import org.wordpress.android.test +import org.wordpress.android.ui.mediapicker.MediaItem.Identifier +import org.wordpress.android.ui.mediapicker.MediaItem.Identifier.RemoteId +import org.wordpress.android.ui.mediapicker.insert.MediaInsertHandler.InsertModel + +@InternalCoroutinesApi +class StockMediaInsertUseCaseTest : BaseUnitTest() { + @Mock lateinit var site: SiteModel + @Mock lateinit var stockMediaStore: StockMediaStore + private lateinit var stockMediaInsertUseCase: StockMediaInsertUseCase + private val url = "wordpress://url" + private val title = "title" + private val name = "name" + private val thumbnail = "image.jpg" + private val stockMediaItem = StockMediaItem( + "id", + name, + title, + url, + "123", + thumbnail + ) + + @Before + fun setUp() { + stockMediaInsertUseCase = StockMediaInsertUseCase(site, stockMediaStore) + } + + @Test + fun `uploads media on insert`() = test { + val itemToInsert = Identifier.StockMediaIdentifier(url, name, title) + val insertedMediaModel = MediaModel() + val mediaId: Long = 10 + insertedMediaModel.mediaId = mediaId + whenever(stockMediaStore.performUploadStockMedia(any(), any())).thenReturn(OnStockMediaUploaded(site, listOf( + insertedMediaModel + ))) + + val result = stockMediaInsertUseCase.insert(listOf(itemToInsert)).toList() + + assertThat(result[0] is InsertModel.Progress).isTrue() + (result[1] as InsertModel.Success).apply { + assertThat(this.identifiers).containsExactly(RemoteId(mediaId)) + } + verify(stockMediaStore).performUploadStockMedia(site, listOf(StockMediaUploadItem(name, title, url))) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/MediaLoaderFactoryTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/loader/MediaLoaderFactoryTest.kt similarity index 59% rename from WordPress/src/test/java/org/wordpress/android/ui/mediapicker/MediaLoaderFactoryTest.kt rename to WordPress/src/test/java/org/wordpress/android/ui/mediapicker/loader/MediaLoaderFactoryTest.kt index cc34ec530df3..ad4125c16c8f 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/MediaLoaderFactoryTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/loader/MediaLoaderFactoryTest.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.mediapicker +package org.wordpress.android.ui.mediapicker.loader import com.nhaarman.mockitokotlin2.whenever import org.assertj.core.api.Assertions.assertThat @@ -8,27 +8,36 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner -import org.wordpress.android.R +import org.wordpress.android.R.string import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.ui.mediapicker.MediaLibraryDataSource.MediaLibraryDataSourceFactory +import org.wordpress.android.ui.mediapicker.MediaPickerSetup import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.DEVICE import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.GIF_LIBRARY import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.STOCK_LIBRARY import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.WP_LIBRARY +import org.wordpress.android.ui.mediapicker.loader.DeviceListBuilder.DeviceListBuilderFactory +import org.wordpress.android.ui.mediapicker.loader.MediaLibraryDataSource.MediaLibraryDataSourceFactory import org.wordpress.android.util.LocaleManagerWrapper @RunWith(MockitoJUnitRunner::class) class MediaLoaderFactoryTest { + @Mock lateinit var deviceListBuilderFactory: DeviceListBuilderFactory @Mock lateinit var deviceListBuilder: DeviceListBuilder @Mock lateinit var mediaLibraryDataSourceFactory: MediaLibraryDataSourceFactory @Mock lateinit var mediaLibraryDataSource: MediaLibraryDataSource + @Mock lateinit var stockMediaDataSource: StockMediaDataSource @Mock lateinit var localeManagerWrapper: LocaleManagerWrapper @Mock lateinit var site: SiteModel private lateinit var mediaLoaderFactory: MediaLoaderFactory @Before fun setUp() { - mediaLoaderFactory = MediaLoaderFactory(deviceListBuilder, mediaLibraryDataSourceFactory, localeManagerWrapper) + mediaLoaderFactory = MediaLoaderFactory( + deviceListBuilderFactory, + mediaLibraryDataSourceFactory, + stockMediaDataSource, + localeManagerWrapper + ) } @Test @@ -42,15 +51,16 @@ class MediaLoaderFactoryTest { systemPickerEnabled = true, editingEnabled = true, queueResults = false, - title = R.string.wp_media_title + defaultSearchView = false, + title = string.wp_media_title ) + whenever(deviceListBuilderFactory.build(setOf())).thenReturn(deviceListBuilder) val mediaLoader = mediaLoaderFactory.build(mediaPickerSetup, site) assertThat(mediaLoader).isEqualTo( MediaLoader( deviceListBuilder, - localeManagerWrapper, - mediaPickerSetup.allowedTypes + localeManagerWrapper ) ) } @@ -66,17 +76,42 @@ class MediaLoaderFactoryTest { systemPickerEnabled = false, editingEnabled = false, queueResults = false, - title = R.string.wp_media_title + defaultSearchView = false, + title = string.wp_media_title ) - whenever(mediaLibraryDataSourceFactory.build(site)).thenReturn(mediaLibraryDataSource) + whenever(mediaLibraryDataSourceFactory.build(site, setOf())).thenReturn(mediaLibraryDataSource) val mediaLoader = mediaLoaderFactory.build(mediaPickerSetup, site) assertThat(mediaLoader).isEqualTo( MediaLoader( mediaLibraryDataSource, - localeManagerWrapper, - mediaPickerSetup.allowedTypes + localeManagerWrapper + ) + ) + } + + @Test + fun `returns stock media source on STOCK_LIBRARY source`() { + val mediaPickerSetup = MediaPickerSetup( + STOCK_LIBRARY, + canMultiselect = true, + requiresStoragePermissions = false, + allowedTypes = setOf(), + cameraEnabled = false, + systemPickerEnabled = false, + editingEnabled = false, + queueResults = false, + defaultSearchView = false, + title = string.wp_media_title + ) + + val mediaLoader = mediaLoaderFactory.build(mediaPickerSetup, site) + + assertThat(mediaLoader).isEqualTo( + MediaLoader( + stockMediaDataSource, + localeManagerWrapper ) ) } @@ -85,22 +120,8 @@ class MediaLoaderFactoryTest { fun `throws exception on not implemented sources`() { assertThatExceptionOfType(NotImplementedError::class.java).isThrownBy { mediaLoaderFactory.build( - MediaPickerSetup(GIF_LIBRARY, - canMultiselect = true, - requiresStoragePermissions = true, - allowedTypes = setOf(), - cameraEnabled = false, - systemPickerEnabled = true, - editingEnabled = true, - queueResults = false, - title = R.string.wp_media_title - ), - site - ) - } - assertThatExceptionOfType(NotImplementedError::class.java).isThrownBy { - mediaLoaderFactory.build( - MediaPickerSetup(STOCK_LIBRARY, + MediaPickerSetup( + GIF_LIBRARY, canMultiselect = true, requiresStoragePermissions = true, allowedTypes = setOf(), @@ -108,7 +129,8 @@ class MediaLoaderFactoryTest { systemPickerEnabled = true, editingEnabled = true, queueResults = false, - title = R.string.wp_media_title + defaultSearchView = false, + title = string.wp_media_title ), site ) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/MediaLoaderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/loader/MediaLoaderTest.kt similarity index 73% rename from WordPress/src/test/java/org/wordpress/android/ui/mediapicker/MediaLoaderTest.kt rename to WordPress/src/test/java/org/wordpress/android/ui/mediapicker/loader/MediaLoaderTest.kt index 4586f64904e0..5a8ec4ba33a2 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/MediaLoaderTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/loader/MediaLoaderTest.kt @@ -1,7 +1,6 @@ -package org.wordpress.android.ui.mediapicker +package org.wordpress.android.ui.mediapicker.loader import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.isNull import com.nhaarman.mockitokotlin2.whenever import kotlinx.coroutines.channels.Channel @@ -14,10 +13,11 @@ import org.junit.Test import org.mockito.Mock import org.wordpress.android.BaseUnitTest import org.wordpress.android.test +import org.wordpress.android.ui.mediapicker.MediaItem import org.wordpress.android.ui.mediapicker.MediaItem.Identifier -import org.wordpress.android.ui.mediapicker.MediaLoader.DomainModel -import org.wordpress.android.ui.mediapicker.MediaLoader.LoadAction -import org.wordpress.android.ui.mediapicker.MediaSource.MediaLoadingResult +import org.wordpress.android.ui.mediapicker.loader.MediaLoader.DomainModel +import org.wordpress.android.ui.mediapicker.loader.MediaLoader.LoadAction +import org.wordpress.android.ui.mediapicker.loader.MediaSource.MediaLoadingResult import org.wordpress.android.ui.mediapicker.MediaType.IMAGE import org.wordpress.android.ui.mediapicker.MediaType.VIDEO import org.wordpress.android.util.LocaleManagerWrapper @@ -34,7 +34,7 @@ class MediaLoaderTest : BaseUnitTest() { @Before fun setUp() { - mediaLoader = MediaLoader(mediaSource, localeManagerWrapper, mediaTypes) + mediaLoader = MediaLoader(mediaSource, localeManagerWrapper) firstMediaItem = MediaItem(identifier1, "url://first_item", "first item", IMAGE, "image/jpeg", 1) secondMediaItem = MediaItem(identifier2, "url://second_item", "second item", VIDEO, "video/mpeg", 2) } @@ -44,12 +44,10 @@ class MediaLoaderTest : BaseUnitTest() { val mediaItems = listOf(firstMediaItem) whenever( mediaSource.load( - mediaTypes, forced = false, loadMore = false ) - ).thenReturn(MediaLoadingResult.Success()) - whenever(mediaSource.get(mediaTypes)).thenReturn(mediaItems) + ).thenReturn(MediaLoadingResult.Success(mediaItems, hasMore = false)) performAction(LoadAction.Start(), true) @@ -59,7 +57,7 @@ class MediaLoaderTest : BaseUnitTest() { @Test fun `shows an error when loading fails`() = withMediaLoader { resultModel, performAction -> val errorMessage = "error" - whenever(mediaSource.load(mediaTypes, forced = false, loadMore = false)).thenReturn( + whenever(mediaSource.load(forced = false, loadMore = false)).thenReturn( MediaLoadingResult.Failure( errorMessage ) @@ -72,14 +70,10 @@ class MediaLoaderTest : BaseUnitTest() { @Test fun `loads next page`() = withMediaLoader { resultModel, performAction -> - val firstPage = MediaLoadingResult.Success(hasMore = true) - val secondPage = MediaLoadingResult.Success() - whenever(mediaSource.load(mediaTypes, forced = false, loadMore = false)).thenReturn(firstPage) - whenever(mediaSource.load(mediaTypes, forced = false, loadMore = true)).thenReturn(secondPage) - whenever(mediaSource.get(mediaTypes)).thenReturn( - listOf(firstMediaItem), - listOf(firstMediaItem, secondMediaItem) - ) + val firstPage = MediaLoadingResult.Success(listOf(firstMediaItem), hasMore = true) + val secondPage = MediaLoadingResult.Success(listOf(firstMediaItem, secondMediaItem)) + whenever(mediaSource.load(forced = false, loadMore = false)).thenReturn(firstPage) + whenever(mediaSource.load(forced = false, loadMore = true)).thenReturn(secondPage) performAction(LoadAction.Start(), true) @@ -92,12 +86,11 @@ class MediaLoaderTest : BaseUnitTest() { @Test fun `shows an error when loading next page fails`() = withMediaLoader { resultModel, performAction -> - val firstPage = MediaLoadingResult.Success(hasMore = true) - whenever(mediaSource.get(mediaTypes)).thenReturn(listOf(firstMediaItem)) + val firstPage = MediaLoadingResult.Success(listOf(firstMediaItem), hasMore = true) val message = "error" val secondPage = MediaLoadingResult.Failure(message) - whenever(mediaSource.load(mediaTypes, forced = false, loadMore = false)).thenReturn(firstPage) - whenever(mediaSource.load(mediaTypes, forced = false, loadMore = true)).thenReturn(secondPage) + whenever(mediaSource.load(forced = false, loadMore = false)).thenReturn(firstPage) + whenever(mediaSource.load(forced = false, loadMore = true)).thenReturn(secondPage) performAction(LoadAction.Start(), true) @@ -110,9 +103,9 @@ class MediaLoaderTest : BaseUnitTest() { @Test fun `refresh overrides data`() = withMediaLoader { resultModel, performAction -> - val firstResult = MediaLoadingResult.Success() - whenever(mediaSource.load(eq(mediaTypes), any(), any())).thenReturn(firstResult) - whenever(mediaSource.get(mediaTypes)).thenReturn(listOf(firstMediaItem), listOf(secondMediaItem)) + val firstResult = MediaLoadingResult.Success(listOf(firstMediaItem)) + val secondResult = MediaLoadingResult.Success(listOf(secondMediaItem)) + whenever(mediaSource.load(any(), any(), isNull())).thenReturn(firstResult, secondResult) performAction(LoadAction.Start(), true) @@ -126,16 +119,15 @@ class MediaLoaderTest : BaseUnitTest() { @Test fun `filters out media item`() = withMediaLoader { resultModel, performAction -> val mediaItems = listOf(firstMediaItem, secondMediaItem) + val filter = "second" + whenever(mediaSource.load(forced = false, loadMore = false)).thenReturn(MediaLoadingResult.Success(mediaItems)) whenever( mediaSource.load( - mediaTypes, forced = false, - loadMore = false + loadMore = false, + filter = filter ) - ).thenReturn(MediaLoadingResult.Success()) - whenever(mediaSource.get(mediaTypes)).thenReturn(mediaItems) - val filter = "second" - whenever(mediaSource.get(mediaTypes, filter)).thenReturn(listOf(secondMediaItem)) + ).thenReturn(MediaLoadingResult.Success(listOf(secondMediaItem))) performAction(LoadAction.Start(), true) @@ -151,16 +143,20 @@ class MediaLoaderTest : BaseUnitTest() { @Test fun `clears filter`() = withMediaLoader { resultModel, performAction -> val mediaItems = listOf(firstMediaItem, secondMediaItem) + val filter = "second" + whenever( + mediaSource.load( + forced = false, + loadMore = false, + filter = filter + ) + ).thenReturn(MediaLoadingResult.Success(listOf(secondMediaItem))) whenever( mediaSource.load( - mediaTypes, forced = false, loadMore = false ) - ).thenReturn(MediaLoadingResult.Success()) - val filter = "second" - whenever(mediaSource.get(eq(mediaTypes), eq(filter))).thenReturn(listOf(secondMediaItem)) - whenever(mediaSource.get(eq(mediaTypes), isNull())).thenReturn(mediaItems) + ).thenReturn(MediaLoadingResult.Success(mediaItems)) performAction(LoadAction.Start(), true) performAction(LoadAction.Filter(filter), true) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/loader/StockMediaDataSourceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/loader/StockMediaDataSourceTest.kt new file mode 100644 index 000000000000..f2ed2826dc87 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/loader/StockMediaDataSourceTest.kt @@ -0,0 +1,90 @@ +package org.wordpress.android.ui.mediapicker.loader + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.verifyZeroInteractions +import com.nhaarman.mockitokotlin2.whenever +import kotlinx.coroutines.InternalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.TEST_DISPATCHER +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.StockMediaModel +import org.wordpress.android.fluxc.store.StockMediaItem +import org.wordpress.android.fluxc.store.StockMediaStore +import org.wordpress.android.fluxc.store.StockMediaStore.OnStockMediaListFetched +import org.wordpress.android.test +import org.wordpress.android.ui.mediapicker.MediaItem +import org.wordpress.android.ui.mediapicker.MediaItem.Identifier.StockMediaIdentifier +import org.wordpress.android.ui.mediapicker.MediaType.IMAGE +import org.wordpress.android.ui.mediapicker.loader.MediaSource.MediaLoadingResult + +@InternalCoroutinesApi +class StockMediaDataSourceTest : BaseUnitTest() { + @Mock lateinit var site: SiteModel + @Mock lateinit var stockMediaStore: StockMediaStore + private lateinit var stockMediaDataSource: StockMediaDataSource + private val url = "wordpress://url" + private val title = "title" + private val name = "name" + private val thumbnail = "image.jpg" + private val stockMediaItem = StockMediaItem( + "id", + name, + title, + url, + "123", + thumbnail + ) + + @Before + fun setUp() { + stockMediaDataSource = StockMediaDataSource(stockMediaStore, TEST_DISPATCHER) + } + + @Test + fun `returns empty list with filter with less than 2 chars`() = test { + val filter = "do" + + val result = stockMediaDataSource.load(forced = false, loadMore = false, filter = filter) + + (result as MediaLoadingResult.Success).apply { + assertThat(this.data).isEmpty() + assertThat(this.hasMore).isFalse() + } + verifyZeroInteractions(stockMediaStore) + } + + @Test + fun `returns success from store with filter with more than 2 chars`() = test { + val filter = "dog" + val loadMore = false + val hasMore = true + whenever(stockMediaStore.fetchStockMedia(filter, loadMore)).thenReturn( + OnStockMediaListFetched(listOf(StockMediaModel()), filter, 1, hasMore) + ) + whenever(stockMediaStore.getStockMedia()).thenReturn(listOf(stockMediaItem)) + + val result = stockMediaDataSource.load(forced = false, loadMore = loadMore, filter = filter) + + (result as MediaLoadingResult.Success).apply { + assertThat(this.data).hasSize(1) + assertThat(this.data).containsExactly( + MediaItem( + StockMediaIdentifier(url, name, title), + url, + name, + IMAGE, + null, + 123 + ) + ) + assertThat(this.hasMore).isTrue() + } + verify(stockMediaStore).fetchStockMedia(any(), any()) + verify(stockMediaStore).getStockMedia() + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModelTest.kt index c13d23a30ec1..3f8ee49751c4 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModelTest.kt @@ -350,7 +350,7 @@ class PhotoPickerViewModelTest : BaseUnitTest() { browserType: MediaBrowserType, hasStoragePermissions: Boolean = true ) { - whenever(permissionsHandler.hasStoragePermission()).thenReturn(hasStoragePermissions) + whenever(permissionsHandler.hasWriteStoragePermission()).thenReturn(hasStoragePermissions) viewModel.start(listOf(), browserType, null, site) whenever(deviceMediaListBuilder.buildDeviceMedia(browserType)).thenReturn(domainModel) viewModel.uiState.observeForever { diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt index bf9dc9550fb6..4ce0c14d0baa 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt @@ -497,7 +497,7 @@ class ReaderDiscoverViewModelTest { tagItems = listOf(TagUiState("", "", false, onTagClicked)), dateLine = "", avatarOrBlavatarUrl = "", - blogName = "", + blogName = mock(), excerpt = "", title = mock(), photoFrameVisibility = false, diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderPostUiStateBuilderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderPostUiStateBuilderTest.kt index 6af318a33ce3..d30cca6a55d5 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderPostUiStateBuilderTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderPostUiStateBuilderTest.kt @@ -489,13 +489,13 @@ class ReaderPostUiStateBuilderTest { } @Test - fun `blogName is not displayed the post doesn't have a blog name`() = test { + fun `default blog name is displayed when the post doesn't have a blog name`() = test { // Arrange val post = createPost(hasBlogName = false) // Act val uiState = mapPostToUiState(post) // Assert - assertThat(uiState.blogName).isNull() + assertThat((uiState.blogName as UiStringRes).stringRes).isEqualTo(R.string.untitled_in_parentheses) } // endregion diff --git a/build.gradle b/build.gradle index 9fd8a62f4140..3c4f7ae4d499 100644 --- a/build.gradle +++ b/build.gradle @@ -129,7 +129,7 @@ ext { androidxWorkVersion = "2.0.1" daggerVersion = '2.22.1' - fluxCVersion = '1.6.22' + fluxCVersion = '1.6.23-beta-1' appCompatVersion = '1.0.2' coreVersion = '1.2.0' diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index ee4c2230cb47..641243b587c1 100644 --- a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -638,6 +638,7 @@ public enum Stat { STORY_POST_SAVE_REMOTELY, STORY_SAVE_ERROR_SNACKBAR_MANAGE_TAPPED, STORY_POST_PUBLISH_TAPPED, + STORY_TEXT_CHANGED, PREPUBLISHING_BOTTOM_SHEET_OPENED, PREPUBLISHING_BOTTOM_SHEET_DISMISSED, FEATURE_ANNOUNCEMENT_SHOWN_ON_APP_UPGRADE, diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java index c061db2549d8..31713032eb60 100644 --- a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java +++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java @@ -1787,6 +1787,8 @@ public static String getEventNameForStat(AnalyticsTracker.Stat stat) { return "story_post_error_snackbar_manage_tapped"; case STORY_POST_PUBLISH_TAPPED: return "story_post_publish_tapped"; + case STORY_TEXT_CHANGED: + return "story_text_changed"; case FEATURE_ANNOUNCEMENT_SHOWN_ON_APP_UPGRADE: case FEATURE_ANNOUNCEMENT_SHOWN_FROM_APP_SETTINGS: return "feature_announcement_shown"; diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java index c3a5eb58b8f7..88f08b68b4c9 100644 --- a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java +++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java @@ -114,7 +114,7 @@ public class GutenbergEditorFragment extends EditorFragmentAbstract implements private boolean mIsJetpackSsoEnabled; private boolean mEditorDidMount; - private GutenbergPropsBuilder mLatestGutenbergPropsBuilder; + private GutenbergPropsBuilder mCurrentGutenbergPropsBuilder; private ProgressDialog mSavingContentProgressDialog; @@ -154,6 +154,7 @@ public void onCreate(Bundle savedInstanceState) { if (getGutenbergContainerFragment() == null) { GutenbergPropsBuilder gutenbergPropsBuilder = getArguments().getParcelable(ARG_GUTENBERG_PROPS_BUILDER); + mCurrentGutenbergPropsBuilder = gutenbergPropsBuilder; FragmentManager fragmentManager = getChildFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); @@ -397,7 +398,7 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d String content = data.getStringExtra(WPGutenbergWebViewActivity.ARG_BLOCK_CONTENT); getGutenbergContainerFragment().replaceUnsupportedBlock(content, blockId); // We need to send latest capabilities as JS side clears them - getGutenbergContainerFragment().updateCapabilities(mLatestGutenbergPropsBuilder); + getGutenbergContainerFragment().updateCapabilities(mCurrentGutenbergPropsBuilder); trackWebViewClosed("save"); } else { trackWebViewClosed("dismiss"); @@ -690,7 +691,7 @@ public void setContent(CharSequence text) { public void updateCapabilities(boolean isJetpackSsoEnabled, GutenbergPropsBuilder gutenbergPropsBuilder) { mIsJetpackSsoEnabled = isJetpackSsoEnabled; - mLatestGutenbergPropsBuilder = gutenbergPropsBuilder; + mCurrentGutenbergPropsBuilder = gutenbergPropsBuilder; getGutenbergContainerFragment().updateCapabilities(gutenbergPropsBuilder); } diff --git a/libs/login/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginSiteAddressFragment.java b/libs/login/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginSiteAddressFragment.java index deb2fd6ead2f..767ae90b256b 100644 --- a/libs/login/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginSiteAddressFragment.java +++ b/libs/login/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginSiteAddressFragment.java @@ -372,7 +372,7 @@ public void onFetchedConnectSiteInfo(OnConnectSiteInfoChecked event) { if (mLoginListener.getLoginMode() == LoginMode.WOO_LOGIN_MODE) { handleConnectSiteInfoForWoo(event.info, hasJetpack); } else { - handleConnectSiteInfoForWordPress(event.info, hasJetpack); + handleConnectSiteInfoForWordPress(event.info); } } } @@ -394,23 +394,21 @@ private void handleConnectSiteInfoForWoo(ConnectSiteInfoPayload siteInfo, boolea } } - private void handleConnectSiteInfoForWordPress(ConnectSiteInfoPayload siteInfo, boolean hasJetpack) { - if (siteInfo.isWPCom || hasJetpack) { - // It's a WordPress.com or a connected Jetpack site + private void handleConnectSiteInfoForWordPress(ConnectSiteInfoPayload siteInfo) { + if (siteInfo.isWPCom) { + // It's a Simple or Atomic site if (mLoginListener.getLoginMode() == LoginMode.SELFHOSTED_ONLY) { // We're only interested in self-hosted sites - if (hasJetpack) { - // If Jetpack site, treat it as self-hosted and start the discovery process - // Note: This also includes Atomic sites + if (siteInfo.hasJetpack) { + // This is an Atomic site, so treat it as self-hosted and start the discovery process initiateDiscovery(); return; } } - // It's a WordPress.com or a connected Jetpack site, so treat it as such endProgressIfNeeded(); mLoginListener.gotWpcomSiteInfo(UrlUtils.removeScheme(siteInfo.url)); } else { - // Not a WordPress.com or a connected Jetpack site + // It's a Jetpack or self-hosted site if (mLoginListener.getLoginMode() == LoginMode.WPCOM_LOGIN_ONLY) { // We're only interested in WordPress.com accounts showError(R.string.enter_wpcom_or_jetpack_site); diff --git a/libs/stories-android b/libs/stories-android index f216350ee5e0..b05f8197dbc1 160000 --- a/libs/stories-android +++ b/libs/stories-android @@ -1 +1 @@ -Subproject commit f216350ee5e09e9dda58cf41e7bb61c730fc0ca9 +Subproject commit b05f8197dbc144fc024379c462c76efbb7c5440c