diff --git a/WordPress/build.gradle b/WordPress/build.gradle index e2db1d6e8ea5..340aa48311c3 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -15,7 +15,6 @@ repositories { google() jcenter() maven { url 'https://zendesk.jfrog.io/zendesk/repo' } - maven { url "https://giphy.bintray.com/giphy-sdk" } maven { url "https://www.jitpack.io" } maven { url "http://dl.bintray.com/terl/lazysodium-maven" } } @@ -68,6 +67,7 @@ android { testInstrumentationRunner 'org.wordpress.android.WordPressTestRunner' buildConfigField "boolean", "OFFER_GUTENBERG", "true" + buildConfigField "boolean", "TENOR_AVAILABLE", "true" } // Gutenberg's dependency - react-native-video is using @@ -88,6 +88,7 @@ android { } versionCode 845 buildConfigField "boolean", "ME_ACTIVITY_AVAILABLE", "false" + buildConfigField "boolean", "TENOR_AVAILABLE", "false" } zalpha { // alpha version - enable experimental features @@ -225,6 +226,7 @@ dependencies { testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0' testImplementation "org.assertj:assertj-core:$assertJVersion" testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.1' + testImplementation 'androidx.test:core:1.2.0' androidTestImplementation 'org.mockito:mockito-android:2.27.0' androidTestImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0' @@ -290,6 +292,8 @@ dependencies { exclude group: 'com.google.dagger' } + implementation 'com.github.Tenor-Inc:tenor-android-core:0.5.1' + lintChecks 'org.wordpress:lint:1.0.1' // Sentry diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index 3855fffeea59..b093bdd0a95f 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -529,7 +529,7 @@ android:label="" android:theme="@style/WordPress.NoActionBar" /> diff --git a/WordPress/src/main/assets/licenses.html b/WordPress/src/main/assets/licenses.html index fb28beaa1cb2..73e104be88dc 100644 --- a/WordPress/src/main/assets/licenses.html +++ b/WordPress/src/main/assets/licenses.html @@ -34,6 +34,7 @@

Additional Libraries

  • okio/okhttp: Copyright 2013 Square, Inc.
  • EventBus: Copyright 2012-2016 Markus Junginger, greenrobot
  • Mobile 4 Media: Copyright 2016, INDExOS
  • +
  • Tenor Android Core: Copyright 2017, Tenor Inc

  • @@ -44,10 +45,5 @@

    Additional Libraries

  • GraphView: Copyright 2013, Jonas Gehring
  • -

    The following is licensed under Mozilla Public License Version 2.0

    - - diff --git a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java index a9edf87b9af5..d29752d0dedf 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java @@ -40,7 +40,7 @@ import org.wordpress.android.ui.domains.DomainRegistrationActivity; import org.wordpress.android.ui.domains.DomainRegistrationDetailsFragment; import org.wordpress.android.ui.domains.DomainSuggestionsFragment; -import org.wordpress.android.ui.giphy.GiphyPickerActivity; +import org.wordpress.android.ui.gif.GifPickerActivity; import org.wordpress.android.ui.history.HistoryAdapter; import org.wordpress.android.ui.history.HistoryDetailContainerFragment; import org.wordpress.android.ui.main.AddContentAdapter; @@ -438,7 +438,7 @@ public interface AppComponent extends AndroidInjector { void inject(JetpackRemoteInstallFragment jetpackRemoteInstallFragment); - void inject(GiphyPickerActivity object); + void inject(GifPickerActivity object); void inject(PlansListAdapter object); diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java index 1a71f80f37ff..15a93cf44de7 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java @@ -5,6 +5,11 @@ import androidx.lifecycle.LiveData; +import com.tenor.android.core.network.ApiClient; +import com.tenor.android.core.network.ApiService; +import com.tenor.android.core.network.IApiClient; + +import org.wordpress.android.BuildConfig; import org.wordpress.android.ui.CommentFullScreenDialogFragment; import org.wordpress.android.ui.accounts.signup.SettingsUsernameChangerFragment; import org.wordpress.android.ui.accounts.signup.UsernameChangerFullScreenDialogFragment; @@ -28,6 +33,8 @@ import org.wordpress.android.ui.stats.refresh.lists.widget.minified.StatsMinifiedWidgetConfigureFragment; import org.wordpress.android.util.wizard.WizardManager; import org.wordpress.android.viewmodel.ContextProvider; +import org.wordpress.android.viewmodel.gif.provider.GifProvider; +import org.wordpress.android.viewmodel.gif.provider.TenorProvider; import org.wordpress.android.viewmodel.helpers.ConnectionStatus; import org.wordpress.android.viewmodel.helpers.ConnectionStatusLiveData; @@ -108,4 +115,12 @@ public static WizardManager provideWizardManager( static LiveData provideConnectionStatusLiveData(Context context) { return new ConnectionStatusLiveData.Factory(context).create(); } + + @Provides + static GifProvider provideGifProvider(Context context) { + ApiService.IBuilder builder = new ApiService.Builder<>(context, IApiClient.class); + builder.apiKey(BuildConfig.TENOR_API_KEY); + ApiClient.init(context, builder); + return new TenorProvider(context, ApiClient.getInstance(context)); + } } diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java index cec149670aa7..f2981c1a7386 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java @@ -37,7 +37,7 @@ import org.wordpress.android.viewmodel.activitylog.ActivityLogViewModel; import org.wordpress.android.viewmodel.domains.DomainRegistrationDetailsViewModel; import org.wordpress.android.viewmodel.domains.DomainSuggestionsViewModel; -import org.wordpress.android.viewmodel.giphy.GiphyPickerViewModel; +import org.wordpress.android.viewmodel.gif.GifPickerViewModel; import org.wordpress.android.viewmodel.history.HistoryViewModel; import org.wordpress.android.viewmodel.main.WPMainActivityViewModel; import org.wordpress.android.viewmodel.pages.PageListViewModel; @@ -218,8 +218,8 @@ abstract class ViewModelModule { @Binds @IntoMap - @ViewModelKey(GiphyPickerViewModel.class) - abstract ViewModel giphyPickerViewModel(GiphyPickerViewModel viewModel); + @ViewModelKey(GifPickerViewModel.class) + abstract ViewModel gifPickerViewModel(GifPickerViewModel viewModel); @Binds @IntoMap 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 f58aac37f04a..8f379d598ce0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java @@ -38,6 +38,7 @@ import org.wordpress.android.ui.comments.CommentsActivity; import org.wordpress.android.ui.domains.DomainRegistrationActivity; import org.wordpress.android.ui.domains.DomainRegistrationActivity.DomainRegistrationPurpose; +import org.wordpress.android.ui.gif.GifPickerActivity; import org.wordpress.android.ui.history.HistoryDetailActivity; import org.wordpress.android.ui.history.HistoryDetailContainerFragment; import org.wordpress.android.ui.history.HistoryListItem.Revision; @@ -173,6 +174,16 @@ public static void showStockMediaPickerForResult(Activity activity, activity.startActivityForResult(intent, requestCode); } + public static void showGifPickerForResult(Activity activity, @NonNull SiteModel site, int requestCode) { + Map properties = new HashMap<>(); + properties.put("from", activity.getClass().getSimpleName()); + AnalyticsTracker.track(Stat.GIF_PICKER_ACCESSED, properties); + + Intent intent = new Intent(activity, GifPickerActivity.class); + intent.putExtra(WordPress.SITE, site); + activity.startActivityForResult(intent, requestCode); + } + public static void startJetpackInstall(Context context, JetpackConnectionSource source, SiteModel site) { Intent intent = new Intent(context, JetpackRemoteInstallActivity.class); intent.putExtra(WordPress.SITE, site); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java b/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java index 9914877774d7..0784fadcb4df 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java @@ -46,6 +46,8 @@ public class RequestCodes { // QuickStart public static final int QUICK_START_REMINDER_RECEIVER = 4000; + public static final int GIF_PICKER = 3200; + // Domain Registration public static final int DOMAIN_REGISTRATION = 5000; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/giphy/GiphyMediaViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/gif/GifMediaViewHolder.kt similarity index 81% rename from WordPress/src/main/java/org/wordpress/android/ui/giphy/GiphyMediaViewHolder.kt rename to WordPress/src/main/java/org/wordpress/android/ui/gif/GifMediaViewHolder.kt index 09b47f52aceb..610256836843 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/giphy/GiphyMediaViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/gif/GifMediaViewHolder.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.giphy +package org.wordpress.android.ui.gif import android.view.LayoutInflater import android.view.View @@ -14,18 +14,18 @@ import org.wordpress.android.util.getDistinct import org.wordpress.android.util.image.ImageManager import org.wordpress.android.util.image.ImageType.PHOTO import org.wordpress.android.util.redirectContextClickToLongPressListener -import org.wordpress.android.viewmodel.giphy.GiphyMediaViewModel +import org.wordpress.android.viewmodel.gif.GifMediaViewModel /** - * Represents a single item in the [GiphyPickerActivity]'s grid (RecyclerView). + * Represents a single item in the [GifPickerActivity]'s grid (RecyclerView). * * This is meant to show a single animated gif. * - * This ViewHolder references a readonly [GiphyMediaViewModel]. It should never update the [GiphyMediaViewModel]. That - * behavior is handled by the [GiphyPickerViewModel]. This is designed this way so that [GiphyPickerViewModel] + * This ViewHolder references a readonly [GifMediaViewModel]. It should never update the [GifMediaViewModel]. That + * behavior is handled by the [GifPickerViewModel]. This is designed this way so that [GifPickerViewModel] * encapsulates all the logic of managing selected items as well as keeping their selection numbers continuous. */ -class GiphyMediaViewHolder( +class GifMediaViewHolder( /** * The [ImageManager] to use for loading an image in to the ImageView */ @@ -35,11 +35,11 @@ class GiphyMediaViewHolder( * * If there is no bound [mediaViewModel], this can mean that there was an API error or this is just a placeholder. */ - private val onClickListener: (GiphyMediaViewModel?) -> Unit, + private val onClickListener: (GifMediaViewModel?) -> Unit, /** * A function that is called when the user performs a long press on the thumbnail */ - private val onLongClickListener: (GiphyMediaViewModel) -> Unit, + private val onLongClickListener: (GifMediaViewModel) -> Unit, /** * The view used for this `ViewHolder`. */ @@ -48,13 +48,13 @@ class GiphyMediaViewHolder( * The dimensions used for the ImageView */ thumbnailViewDimensions: ThumbnailViewDimensions -) : LifecycleOwnerViewHolder(itemView) { +) : LifecycleOwnerViewHolder(itemView) { data class ThumbnailViewDimensions(val width: Int, val height: Int) private val thumbnailView: ImageView = itemView.image_thumbnail private val selectionNumberTextView: TextView = itemView.text_selection_count - private var mediaViewModel: GiphyMediaViewModel? = null + private var mediaViewModel: GifMediaViewModel? = null init { thumbnailView.apply { @@ -72,13 +72,13 @@ class GiphyMediaViewHolder( } /** - * Update the views to use the given [GiphyMediaViewModel] + * Update the views to use the given [GifMediaViewModel] * * The [mediaViewModel] is optional because we enable placeholders in the paged list created by - * [org.wordpress.android.viewmodel.giphy.GiphyPickerViewModel]. This causes null values to be bound to - * [GiphyMediaViewHolder] instances. + * [org.wordpress.android.viewmodel.gif.GifPickerViewModel]. This causes null values to be bound to + * [GifMediaViewHolder] instances. */ - override fun bind(item: GiphyMediaViewModel?) { + override fun bind(item: GifMediaViewModel?) { super.bind(item) this.mediaViewModel = item @@ -137,19 +137,19 @@ class GiphyMediaViewHolder( private const val THUMBNAIL_SCALE_SELECTED: Float = 0.8f /** - * Create the layout and a new instance of [GiphyMediaViewHolder] + * Create the layout and a new instance of [GifMediaViewHolder] */ fun create( imageManager: ImageManager, - onClickListener: (GiphyMediaViewModel?) -> Unit, - onLongClickListener: (GiphyMediaViewModel) -> Unit, + onClickListener: (GifMediaViewModel?) -> Unit, + onLongClickListener: (GifMediaViewModel) -> Unit, parent: ViewGroup, thumbnailViewDimensions: ThumbnailViewDimensions - ): GiphyMediaViewHolder { + ): GifMediaViewHolder { // We are intentionally reusing this layout since the UI is very similar. val view = LayoutInflater.from(parent.context) .inflate(R.layout.media_picker_thumbnail, parent, false) - return GiphyMediaViewHolder( + return GifMediaViewHolder( imageManager = imageManager, onClickListener = onClickListener, onLongClickListener = onLongClickListener, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/giphy/GiphyPickerActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/gif/GifPickerActivity.kt similarity index 87% rename from WordPress/src/main/java/org/wordpress/android/ui/giphy/GiphyPickerActivity.kt rename to WordPress/src/main/java/org/wordpress/android/ui/gif/GifPickerActivity.kt index c977e783fdc9..babf2dc45000 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/giphy/GiphyPickerActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/gif/GifPickerActivity.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.giphy +package org.wordpress.android.ui.gif import android.app.Activity import android.content.Intent @@ -19,39 +19,39 @@ import org.wordpress.android.WordPress import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.ui.ActionableEmptyView +import org.wordpress.android.ui.gif.GifMediaViewHolder.ThumbnailViewDimensions import org.wordpress.android.ui.LocaleAwareActivity -import org.wordpress.android.ui.giphy.GiphyMediaViewHolder.ThumbnailViewDimensions import org.wordpress.android.ui.media.MediaPreviewActivity import org.wordpress.android.util.AniUtils import org.wordpress.android.util.DisplayUtils import org.wordpress.android.util.ToastUtils +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.util.getDistinct import org.wordpress.android.util.image.ImageManager import org.wordpress.android.viewmodel.ViewModelFactory -import org.wordpress.android.viewmodel.giphy.GiphyMediaViewModel -import org.wordpress.android.viewmodel.giphy.GiphyPickerViewModel -import org.wordpress.android.viewmodel.giphy.GiphyPickerViewModel.EmptyDisplayMode -import org.wordpress.android.viewmodel.giphy.GiphyPickerViewModel.State +import org.wordpress.android.viewmodel.gif.GifMediaViewModel +import org.wordpress.android.viewmodel.gif.GifPickerViewModel +import org.wordpress.android.viewmodel.gif.GifPickerViewModel.EmptyDisplayMode +import org.wordpress.android.viewmodel.gif.GifPickerViewModel.State import javax.inject.Inject /** - * Allows searching of gifs from Giphy - * - * Important: Giphy is currently disabled everywhere. We are planning to replace it with a different service provider. + * Allows searching of gifs from a giving provider */ -class GiphyPickerActivity : LocaleAwareActivity() { +class GifPickerActivity : LocaleAwareActivity() { /** - * Used for loading images in [GiphyMediaViewHolder] + * Used for loading images in [GifMediaViewHolder] */ @Inject lateinit var imageManager: ImageManager @Inject lateinit var viewModelFactory: ViewModelFactory + @Inject lateinit var analyticsTrackerWrapper: AnalyticsTrackerWrapper - private lateinit var viewModel: GiphyPickerViewModel + private lateinit var viewModel: GifPickerViewModel private val gridColumnCount: Int by lazy { if (DisplayUtils.isLandscape(this)) 4 else 3 } /** - * Passed to the [GiphyMediaViewHolder] which will be used as its dimensions + * Passed to the [GifMediaViewHolder] which will be used as its dimensions */ private val thumbnailViewDimensions: ThumbnailViewDimensions by lazy { val width = DisplayUtils.getDisplayPixelWidth(this) / gridColumnCount @@ -64,7 +64,7 @@ class GiphyPickerActivity : LocaleAwareActivity() { val site = intent.getSerializableExtra(WordPress.SITE) as SiteModel - viewModel = ViewModelProviders.of(this, viewModelFactory).get(GiphyPickerViewModel::class.java) + viewModel = ViewModelProviders.of(this, viewModelFactory).get(GifPickerViewModel::class.java) viewModel.setup(site) // We are intentionally reusing this layout since the UI is very similar. @@ -91,10 +91,10 @@ class GiphyPickerActivity : LocaleAwareActivity() { } /** - * Configure the RecyclerView to use [GiphyPickerPagedListAdapter] and display the items in a grid + * Configure the RecyclerView to use [GifPickerPagedListAdapter] and display the items in a grid */ private fun initializeRecyclerView() { - val pagedListAdapter = GiphyPickerPagedListAdapter( + val pagedListAdapter = GifPickerPagedListAdapter( imageManager = imageManager, thumbnailViewDimensions = thumbnailViewDimensions, onMediaViewClickListener = { mediaViewModel -> @@ -110,7 +110,7 @@ class GiphyPickerActivity : LocaleAwareActivity() { ) recycler.apply { - layoutManager = GridLayoutManager(this@GiphyPickerActivity, gridColumnCount) + layoutManager = GridLayoutManager(this@GifPickerActivity, gridColumnCount) adapter = pagedListAdapter } @@ -124,7 +124,7 @@ class GiphyPickerActivity : LocaleAwareActivity() { * Configure the search view to execute search when the keyboard's Done button is pressed. */ private fun initializeSearchView() { - search_view.queryHint = getString(R.string.giphy_picker_search_hint) + search_view.queryHint = getString(R.string.gif_picker_search_hint) search_view.setOnQueryTextListener(object : OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { @@ -150,7 +150,7 @@ class GiphyPickerActivity : LocaleAwareActivity() { } /** - * Configure the selection bar and its labels when the [GiphyPickerViewModel] selected items change + * Configure the selection bar and its labels when the [GifPickerViewModel] selected items change */ private fun initializeSelectionBar() { viewModel.selectionBarIsVisible.observe(this, Observer { @@ -200,8 +200,8 @@ class GiphyPickerActivity : LocaleAwareActivity() { val emptyView: ActionableEmptyView = actionable_empty_view emptyView.run { image.setImageResource(R.drawable.img_illustration_media_105dp) - bottomImage.setImageResource(R.drawable.img_giphy_100dp) - bottomImage.contentDescription = getString(R.string.giphy_powered_by_giphy) + bottomImage.setImageResource(R.drawable.img_tenor_100dp) + bottomImage.contentDescription = getString(R.string.gif_powered_by_tenor) } viewModel.emptyDisplayMode.getDistinct().observe(this, Observer { emptyDisplayMode -> @@ -214,7 +214,7 @@ class GiphyPickerActivity : LocaleAwareActivity() { updateLayoutForSearch(isSearching = true, topMargin = 0) visibility = View.VISIBLE - title.setText(R.string.giphy_picker_empty_search_list) + title.setText(R.string.gif_picker_empty_search_list) image.visibility = View.GONE bottomImage.visibility = View.GONE } @@ -224,7 +224,7 @@ class GiphyPickerActivity : LocaleAwareActivity() { updateLayoutForSearch(isSearching = false, topMargin = 0) visibility = View.VISIBLE - title.setText(R.string.giphy_picker_initial_empty_text) + title.setText(R.string.gif_picker_initial_empty_text) image.visibility = View.VISIBLE bottomImage.visibility = View.VISIBLE } @@ -251,8 +251,8 @@ class GiphyPickerActivity : LocaleAwareActivity() { event ?: return@Observer ToastUtils.showToast( - this@GiphyPickerActivity, - R.string.giphy_picker_endless_scroll_network_error, + this@GifPickerActivity, + R.string.gif_picker_endless_scroll_network_error, ToastUtils.Duration.LONG ) }) @@ -275,7 +275,7 @@ class GiphyPickerActivity : LocaleAwareActivity() { * * @param mediaViewModels A non-empty list */ - private fun showPreview(mediaViewModels: List) { + private fun showPreview(mediaViewModels: List) { check(mediaViewModels.isNotEmpty()) val uris = mediaViewModels.map { it.previewImageUri.toString() } @@ -299,7 +299,7 @@ class GiphyPickerActivity : LocaleAwareActivity() { finish() } else if (result?.errorMessageStringResId != null) { ToastUtils.showToast( - this@GiphyPickerActivity, + this@GifPickerActivity, result.errorMessageStringResId, ToastUtils.Duration.SHORT ) @@ -308,7 +308,7 @@ class GiphyPickerActivity : LocaleAwareActivity() { } /** - * Set up enabling/disabling of controls depending on the current [GiphyPickerViewModel.State]: + * Set up enabling/disabling of controls depending on the current [GifPickerViewModel.State]: * * - [State.IDLE]: All normal functions are allowed * - [State.DOWNLOADING] or [State.FINISHED]: "Add", "Preview", searching, and selecting are disabled @@ -346,7 +346,7 @@ class GiphyPickerActivity : LocaleAwareActivity() { } val properties = mapOf("number_of_media_selected" to mediaLocalIds.size) - AnalyticsTracker.track(AnalyticsTracker.Stat.GIPHY_PICKER_DOWNLOADED, properties) + analyticsTrackerWrapper.track(AnalyticsTracker.Stat.GIF_PICKER_DOWNLOADED, properties) } /** diff --git a/WordPress/src/main/java/org/wordpress/android/ui/gif/GifPickerPagedListAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/gif/GifPickerPagedListAdapter.kt new file mode 100644 index 000000000000..29350ba3c590 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/gif/GifPickerPagedListAdapter.kt @@ -0,0 +1,50 @@ +package org.wordpress.android.ui.gif + +import android.view.ViewGroup +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.DiffUtil.ItemCallback +import org.wordpress.android.ui.gif.GifMediaViewHolder.ThumbnailViewDimensions +import org.wordpress.android.util.image.ImageManager +import org.wordpress.android.viewmodel.gif.GifMediaViewModel + +/** + * An [RecyclerView] adapter to be used with the [PagedList] created by [GifPickerViewModel] + */ +class GifPickerPagedListAdapter( + private val imageManager: ImageManager, + private val thumbnailViewDimensions: ThumbnailViewDimensions, + private val onMediaViewClickListener: (GifMediaViewModel?) -> Unit, + private val onMediaViewLongClickListener: (GifMediaViewModel) -> Unit +) : PagedListAdapter(DIFF_CALLBACK) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GifMediaViewHolder { + return GifMediaViewHolder.create( + imageManager = imageManager, + onClickListener = onMediaViewClickListener, + onLongClickListener = onMediaViewLongClickListener, + parent = parent, + thumbnailViewDimensions = thumbnailViewDimensions + ) + } + + override fun onBindViewHolder(holder: GifMediaViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + companion object { + private val DIFF_CALLBACK = object : ItemCallback() { + override fun areItemsTheSame(oldItem: GifMediaViewModel, newItem: GifMediaViewModel): Boolean { + return oldItem.id == newItem.id + } + + /** + * Always assume that two similar [GifMediaViewModel] objects always have the same content. + * + * It is probably extremely unlikely that GIFs will change while the user is performing + * a search. + */ + override fun areContentsTheSame(oldItem: GifMediaViewModel, newItem: GifMediaViewModel): Boolean { + return true + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/giphy/LifecycleOwnerViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/gif/LifecycleOwnerViewHolder.kt similarity index 99% rename from WordPress/src/main/java/org/wordpress/android/ui/giphy/LifecycleOwnerViewHolder.kt rename to WordPress/src/main/java/org/wordpress/android/ui/gif/LifecycleOwnerViewHolder.kt index 7c207dc25c48..60b213c7a4b8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/giphy/LifecycleOwnerViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/gif/LifecycleOwnerViewHolder.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.giphy +package org.wordpress.android.ui.gif import android.view.View import android.view.View.OnAttachStateChangeListener diff --git a/WordPress/src/main/java/org/wordpress/android/ui/giphy/GiphyPickerPagedListAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/giphy/GiphyPickerPagedListAdapter.kt deleted file mode 100644 index 0087e18a2731..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/giphy/GiphyPickerPagedListAdapter.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.wordpress.android.ui.giphy - -import android.view.ViewGroup -import androidx.paging.PagedListAdapter -import androidx.recyclerview.widget.DiffUtil.ItemCallback -import org.wordpress.android.ui.giphy.GiphyMediaViewHolder.ThumbnailViewDimensions -import org.wordpress.android.util.image.ImageManager -import org.wordpress.android.viewmodel.giphy.GiphyMediaViewModel - -/** - * An [RecyclerView] adapter to be used with the [PagedList] created by [GiphyPickerViewModel] - */ -class GiphyPickerPagedListAdapter( - private val imageManager: ImageManager, - private val thumbnailViewDimensions: ThumbnailViewDimensions, - private val onMediaViewClickListener: (GiphyMediaViewModel?) -> Unit, - private val onMediaViewLongClickListener: (GiphyMediaViewModel) -> Unit -) : PagedListAdapter(DIFF_CALLBACK) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GiphyMediaViewHolder { - return GiphyMediaViewHolder.create( - imageManager = imageManager, - onClickListener = onMediaViewClickListener, - onLongClickListener = onMediaViewLongClickListener, - parent = parent, - thumbnailViewDimensions = thumbnailViewDimensions - ) - } - - override fun onBindViewHolder(holder: GiphyMediaViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - companion object { - private val DIFF_CALLBACK = object : ItemCallback() { - override fun areItemsTheSame(oldItem: GiphyMediaViewModel, newItem: GiphyMediaViewModel): Boolean { - return oldItem.id == newItem.id - } - - /** - * Always assume that two similar [GiphyMediaViewModel] objects always have the same content. - * - * It is probably extremely unlikely that GIFs from Giphy will change while the user is performing - * a search. - */ - override fun areContentsTheSame(oldItem: GiphyMediaViewModel, newItem: GiphyMediaViewModel): Boolean { - return true - } - } - } -} 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 112d828464df..fad5d85fb0c2 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 @@ -63,6 +63,7 @@ import org.wordpress.android.ui.ActivityLauncher; import org.wordpress.android.ui.LocaleAwareActivity; import org.wordpress.android.ui.RequestCodes; +import org.wordpress.android.ui.gif.GifPickerActivity; import org.wordpress.android.ui.media.MediaGridFragment.MediaFilter; import org.wordpress.android.ui.media.MediaGridFragment.MediaGridListener; import org.wordpress.android.ui.media.services.MediaDeleteService; @@ -134,7 +135,8 @@ private enum AddMenuItem { ITEM_CAPTURE_VIDEO, ITEM_CHOOSE_PHOTO, ITEM_CHOOSE_VIDEO, - ITEM_CHOOSE_STOCK_MEDIA + ITEM_CHOOSE_STOCK_MEDIA, + ITEM_CHOOSE_GIF } @Override @@ -466,6 +468,19 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { reloadMediaGrid(); } break; + case RequestCodes.GIF_PICKER: + if (resultCode == RESULT_OK + && data.hasExtra(GifPickerActivity.KEY_SAVED_MEDIA_MODEL_LOCAL_IDS)) { + int[] mediaLocalIds = data.getIntArrayExtra(GifPickerActivity.KEY_SAVED_MEDIA_MODEL_LOCAL_IDS); + + ArrayList mediaModels = new ArrayList<>(); + for (int localId : mediaLocalIds) { + mediaModels.add(mMediaStore.getMediaWithLocalId(localId)); + } + + addMediaToUploadService(mediaModels); + } + break; } } @@ -872,6 +887,14 @@ public void showAddMediaPopup() { }); } + if (mBrowserType.isBrowser() && BuildConfig.TENOR_AVAILABLE) { + popup.getMenu().add(R.string.photo_picker_gif).setOnMenuItemClickListener( + item -> { + doAddMediaItemClicked(AddMenuItem.ITEM_CHOOSE_GIF); + return true; + }); + } + popup.show(); } @@ -909,6 +932,9 @@ private void doAddMediaItemClicked(@NonNull AddMenuItem item) { ActivityLauncher.showStockMediaPickerForResult(this, mSite, RequestCodes.STOCK_MEDIA_PICKER_MULTI_SELECT); break; + case ITEM_CHOOSE_GIF: + ActivityLauncher.showGifPickerForResult(this, mSite, RequestCodes.GIF_PICKER); + break; } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.java index 82ff18776f17..7b3cbb4032d4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.java @@ -24,6 +24,7 @@ import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import org.wordpress.android.BuildConfig; import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; @@ -60,7 +61,8 @@ public enum PhotoPickerIcon { ANDROID_CAPTURE_VIDEO(true), ANDROID_CHOOSE_PHOTO_OR_VIDEO(true), WP_MEDIA(false), - STOCK_MEDIA(true); + STOCK_MEDIA(true), + GIF(true); private boolean mRequiresUploadPermission; @@ -257,6 +259,7 @@ public void doIconClicked(@NonNull PhotoPickerIcon icon) { AnalyticsTracker.track(AnalyticsTracker.Stat.MEDIA_PICKER_OPEN_WP_MEDIA); break; case STOCK_MEDIA: + case GIF: break; } @@ -297,6 +300,14 @@ public boolean onMenuItemClick(MenuItem item) { return true; } }); + + if (BuildConfig.TENOR_AVAILABLE) { + MenuItem itemGif = popup.getMenu().add(R.string.photo_picker_gif); + itemGif.setOnMenuItemClickListener(item -> { + doIconClicked(PhotoPickerIcon.GIF); + return true; + }); + } } popup.show(); 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 27bc02042eb4..d607bd0b661d 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 @@ -101,6 +101,7 @@ import org.wordpress.android.ui.PagePostCreationSourcesDetail; import org.wordpress.android.ui.RequestCodes; import org.wordpress.android.ui.Shortcut; +import org.wordpress.android.ui.gif.GifPickerActivity; import org.wordpress.android.ui.history.HistoryListItem.Revision; import org.wordpress.android.ui.media.MediaBrowserActivity; import org.wordpress.android.ui.media.MediaBrowserType; @@ -920,6 +921,9 @@ public void onPhotoPickerIconClicked(@NonNull PhotoPickerIcon icon, boolean allo ActivityLauncher.showStockMediaPickerForResult( this, mSite, RequestCodes.STOCK_MEDIA_PICKER_MULTI_SELECT); break; + case GIF: + ActivityLauncher.showGifPickerForResult(this, mSite, RequestCodes.GIF_PICKER); + break; } } else { WPSnackbar.make(findViewById(R.id.editor_activity), R.string.media_error_no_permission_upload, @@ -2259,6 +2263,12 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { .addExistingMediaToEditorAsync(AddExistingMediaSource.STOCK_PHOTO_LIBRARY, mediaIds); } break; + case RequestCodes.GIF_PICKER: + if (data.hasExtra(GifPickerActivity.KEY_SAVED_MEDIA_MODEL_LOCAL_IDS)) { + int[] localIds = data.getIntArrayExtra(GifPickerActivity.KEY_SAVED_MEDIA_MODEL_LOCAL_IDS); + mEditorMedia.addGifMediaToPostAsync(localIds); + } + break; case RequestCodes.HISTORY_DETAIL: if (data.hasExtra(KEY_REVISION)) { mViewPager.setCurrentItem(PAGE_CONTENT); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/EditorMedia.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/EditorMedia.kt index 0267ab08af3f..e885e5b411b8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/EditorMedia.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/EditorMedia.kt @@ -138,7 +138,7 @@ class EditorMedia @Inject constructor( /** * This won't create a MediaModel. It assumes the model was already created. */ - fun addMediaFromGiphyToPostAsync(localMediaIds: IntArray) { + fun addGifMediaToPostAsync(localMediaIds: IntArray) { launch { addLocalMediaToPostUseCase.addLocalMediaToEditorAsync( localMediaIds.toList(), diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/CoroutineScopedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/CoroutineScopedViewModel.kt similarity index 97% rename from WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/CoroutineScopedViewModel.kt rename to WordPress/src/main/java/org/wordpress/android/viewmodel/gif/CoroutineScopedViewModel.kt index bc33fb90d74d..475e7b23229b 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/CoroutineScopedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/CoroutineScopedViewModel.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.viewmodel.giphy +package org.wordpress.android.viewmodel.gif import androidx.lifecycle.ViewModel import kotlinx.coroutines.CoroutineScope diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyMediaFetcher.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifMediaFetcher.kt similarity index 80% rename from WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyMediaFetcher.kt rename to WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifMediaFetcher.kt index 38c617edfe2c..4d5073c4b6ab 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyMediaFetcher.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifMediaFetcher.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.viewmodel.giphy +package org.wordpress.android.viewmodel.gif import android.content.Context import android.webkit.MimeTypeMap @@ -18,13 +18,13 @@ import org.wordpress.android.util.WPMediaUtils import javax.inject.Inject /** - * Downloads [GiphyMediaViewModel.largeImageUri] objects and saves them as [MediaModel] + * Downloads [GifMediaViewModel.largeImageUri] objects and saves them as [MediaModel] * * The download happens concurrently and primarily uses [Dispatchers.IO]. This means that we are limited by the number * of threads backed by that `CoroutineDispatcher`. In the future, we should probably add a limit to the number of * GIFs that users can select to reduce the likelihood of OOM exceptions. */ -class GiphyMediaFetcher @Inject constructor( +class GifMediaFetcher @Inject constructor( private val context: Context, private val mediaStore: MediaStore, private val dispatcher: Dispatcher @@ -38,22 +38,22 @@ class GiphyMediaFetcher @Inject constructor( */ @Throws suspend fun fetchAndSave( - giphyMediaViewModels: List, + gifMediaViewModels: List, site: SiteModel ): List = coroutineScope { - // Execute [fetchAndSave] for all giphyMediaViewModels first so that they are queued and executed in the + // Execute [fetchAndSave] for all gifMediaViewModels first so that they are queued and executed in the // background. We'll call `await()` once they are queued. - return@coroutineScope giphyMediaViewModels.map { - fetchAndSave(scope = this, giphyMediaViewModel = it, site = site) + return@coroutineScope gifMediaViewModels.map { + fetchAndSave(scope = this, gifMediaViewModel = it, site = site) }.map { it.await() } } private fun fetchAndSave( scope: CoroutineScope, - giphyMediaViewModel: GiphyMediaViewModel, + gifMediaViewModel: GifMediaViewModel, site: SiteModel ): Deferred = scope.async(Dispatchers.IO) { - val uri = giphyMediaViewModel.largeImageUri + val uri = gifMediaViewModel.largeImageUri // No need to log the Exception here. The underlying method that is used, [MediaUtils.downloadExternalMedia] // already logs any errors. val downloadedUri = WPMediaUtils.fetchMedia(context, uri) ?: throw Exception("Failed to download the image.") @@ -65,7 +65,7 @@ class GiphyMediaFetcher @Inject constructor( val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension) val mediaModel = FluxCUtils.mediaModelFromLocalUri(context, downloadedUri, mimeType, mediaStore, site.id) - mediaModel.title = giphyMediaViewModel.title + mediaModel.title = gifMediaViewModel.title dispatcher.dispatch(MediaActionBuilder.newUpdateMediaAction(mediaModel)) return@async mediaModel diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyMediaViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifMediaViewModel.kt similarity index 59% rename from WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyMediaViewModel.kt rename to WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifMediaViewModel.kt index 24a756e717a1..b0ff105ef692 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyMediaViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifMediaViewModel.kt @@ -1,23 +1,19 @@ -package org.wordpress.android.viewmodel.giphy +package org.wordpress.android.viewmodel.gif import android.net.Uri import androidx.lifecycle.LiveData /** - * A data-representation of [GiphyMediaViewHolder] + * A data-representation of [GifMediaViewHolder] * - * The values of this class comes from Giphy's [Media] model. The [Media] object is big so we use this class to - * only keep a minimal amount of memory. This also hides the complexity of navigating the values of [Media]. + * The values of this class comes from [GifProvider] as a list. * - * This class also houses the selection status. The [GiphyMediaViewHolder] observes the [isSelected] and + * This class also houses the selection status. The [GifMediaViewHolder] observes the [isSelected] and * [selectionNumber] properties to update itself. - * - * See the [Giphy API docs](https://developers.giphy.com/docs/) for more information on what a [Media] object contains. - * Search for "The GIF Object" section. */ -interface GiphyMediaViewModel { +interface GifMediaViewModel { /** - * The id from Giphy's [Media] + * The GIF unique id */ val id: String /** @@ -37,7 +33,7 @@ interface GiphyMediaViewModel { */ val largeImageUri: Uri /** - * The title that appears on giphy.com + * The title that appears on the GIF */ val title: String /** diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyPickerDataSource.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifPickerDataSource.kt similarity index 52% rename from WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyPickerDataSource.kt rename to WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifPickerDataSource.kt index 4fe6bcc45712..72ce9ef85cdc 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyPickerDataSource.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifPickerDataSource.kt @@ -1,31 +1,33 @@ -package org.wordpress.android.viewmodel.giphy +package org.wordpress.android.viewmodel.gif import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.paging.PositionalDataSource +import org.wordpress.android.viewmodel.gif.provider.GifProvider /** - * The PagedListDataSource that is created and managed by [GiphyPickerDataSourceFactory] + * The PagedListDataSource that is created and managed by [GifPickerDataSourceFactory] * * This performs paged API requests using the [apiClient]. A new instance of this class must be created if the * [searchQuery] is changed by the user. */ -class GiphyPickerDataSource( +class GifPickerDataSource( + private val gifProvider: GifProvider, private val searchQuery: String -) : PositionalDataSource() { +) : PositionalDataSource() { /** * The data structure used for storing failed [loadRange] calls so they can be retried later. */ private data class RangeLoadArguments( val params: LoadRangeParams, - val callback: LoadRangeCallback + val callback: LoadRangeCallback ) /** * The error received when [loadInitial] fails. * * Unlike [rangeLoadErrorEvent], this is not a [LiveData] because the consumer of this method - * [GiphyPickerViewModel] simply uses it to check for null values and reacts to a different event. + * [GifPickerViewModel] simply uses it to check for null values and reacts to a different event. * * This is cleared when [loadInitial] is started. */ @@ -49,39 +51,72 @@ class GiphyPickerDataSource( private val failedRangeLoadArguments = mutableListOf() /** - * Always the load the first page (startingPosition = 0) from the Giphy API + * Always the load the first page (startingPosition = 0) from the Gif API * - * The [GiphyPickerDataSourceFactory] recreates [GiphyPickerDataSource] instances whenever a new [searchQuery] + * The [GifPickerDataSourceFactory] recreates [GifPickerDataSource] instances whenever a new [searchQuery] * is queued. The [LoadInitialParams.requestedStartPosition] may have a value that is only valid for the * previous [searchQuery]. If that value is greater than the total search results of the new [searchQuery], * a crash will happen. * - * Using `0` as the `startPosition` forces the [GiphyPickerDataSource] consumer to reset the list (UI) from the + * Using `0` as the `startPosition` forces the [GifPickerDataSource] consumer to reset the list (UI) from the * top. */ - override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { + override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { val startPosition = 0 initialLoadError = null _rangeLoadErrorEvent.postValue(null) - // Do not do any API call if the [searchQuery] is empty - if (searchQuery.isBlank()) { - callback.onResult(emptyList(), startPosition, 0) - return + when { + // Do not do any API call if the [searchQuery] is empty + searchQuery.isBlank() -> callback.onResult(emptyList(), startPosition, 0) + else -> requestInitialSearch(params, callback) } + } + + /** + * Basic search request handling to initialize the list for the loadInitial function + * + * If no next position is informed by the GIF provider, the total count will be set with the size of the GIF list + */ + private fun requestInitialSearch( + params: LoadInitialParams, + callback: LoadInitialCallback + ) { + val startPosition = 0 + gifProvider.search(searchQuery, startPosition, params.requestedLoadSize, + onSuccess = { gifs, nextPosition -> + val totalCount = when { + // nextPosition should never be smaller than the number of GIFs returned + nextPosition == null || nextPosition < gifs.size -> gifs.size + else -> nextPosition + } - callback.onResult(emptyList(), startPosition, 0) + callback.onResult(gifs, startPosition, totalCount) + }, + onFailure = { + initialLoadError = it + callback.onResult(emptyList(), startPosition, 0) + } + ) } /** - * Load a given range of items ([params]) from the Giphy API. + * Load a given range of items ([params]) from the Gif API. * * Errors are dispatched to [rangeLoadErrorEvent]. If successful, previously failed calls of this method are * automatically retried using [retryAllFailedRangeLoads]. */ - override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) { - // Logic removed for now. + override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) { + gifProvider.search(searchQuery, params.startPosition, params.loadSize, + onSuccess = { gifs, _ -> + callback.onResult(gifs) + retryAllFailedRangeLoads() + }, + onFailure = { + failedRangeLoadArguments.add(RangeLoadArguments(params, callback)) + if (_rangeLoadErrorEvent.value == null) _rangeLoadErrorEvent.value = it + }) } /** diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyPickerDataSourceFactory.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifPickerDataSourceFactory.kt similarity index 51% rename from WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyPickerDataSourceFactory.kt rename to WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifPickerDataSourceFactory.kt index 2134355e1a9e..30546f9a4177 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyPickerDataSourceFactory.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifPickerDataSourceFactory.kt @@ -1,26 +1,29 @@ -package org.wordpress.android.viewmodel.giphy +package org.wordpress.android.viewmodel.gif import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.paging.DataSource import androidx.paging.DataSource.Factory +import org.wordpress.android.viewmodel.gif.provider.GifProvider import javax.inject.Inject /** - * Creates instances of [GiphyPickerDataSource] + * Creates instances of [GifPickerDataSource] * * Whenever the [searchQuery] is changed: * - * 1. The last [GiphyPickerDataSource] is invalidated - * 2. The [LivePagedListBuilder] will create a new [GiphyPickerDataSource] by calling [create] - * 3. The new [GiphyPickerDataSource] will start another paged API request + * 1. The last [GifPickerDataSource] is invalidated + * 2. The [LivePagedListBuilder] will create a new [GifPickerDataSource] by calling [create] + * 3. The new [GifPickerDataSource] will start another paged API request */ -class GiphyPickerDataSourceFactory @Inject constructor() : Factory() { +class GifPickerDataSourceFactory @Inject constructor( + private val gifProvider: GifProvider +) : Factory() { /** * The active search query. * - * When changed, the current [GiphyPickerDataSource] will be invalidated. A new API search will be performed. + * When changed, the current [GifPickerDataSource] will be invalidated. A new API search will be performed. */ var searchQuery: String = "" set(value) { @@ -33,26 +36,26 @@ class GiphyPickerDataSourceFactory @Inject constructor() : Factory() + private val dataSource = MutableLiveData() /** - * The [GiphyPickerDataSource.initialLoadError] of the current [dataSource] + * The [GifPickerDataSource.initialLoadError] of the current [dataSource] */ val initialLoadError: Throwable? get() = dataSource.value?.initialLoadError /** - * The [GiphyPickerDataSource.rangeLoadErrorEvent] of the current [dataSource] + * The [GifPickerDataSource.rangeLoadErrorEvent] of the current [dataSource] */ val rangeLoadErrorEvent: LiveData = Transformations.switchMap(dataSource) { it.rangeLoadErrorEvent } /** * Retries all previously failed page loads. * - * @see [GiphyPickerDataSource.retryAllFailedRangeLoads] + * @see [GifPickerDataSource.retryAllFailedRangeLoads] */ fun retryAllFailedRangeLoads() = dataSource.value?.retryAllFailedRangeLoads() - override fun create(): DataSource { - val dataSource = GiphyPickerDataSource(searchQuery) + override fun create(): DataSource { + val dataSource = GifPickerDataSource(gifProvider, searchQuery) this.dataSource.postValue(dataSource) return dataSource } diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyPickerViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifPickerViewModel.kt similarity index 81% rename from WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyPickerViewModel.kt rename to WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifPickerViewModel.kt index 7661aabff81f..69d2a03acddd 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/GiphyPickerViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/GifPickerViewModel.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.viewmodel.giphy +package org.wordpress.android.viewmodel.gif import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -19,27 +19,29 @@ import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.fluxc.model.MediaModel import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.util.NetworkUtilsWrapper +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.util.getDistinct import org.wordpress.android.viewmodel.SingleLiveEvent import javax.inject.Inject /** - * Holds the data for [org.wordpress.android.ui.giphy.GiphyPickerActivity] + * Holds the data for [org.wordpress.android.ui.gif.GifPickerActivity] * * This creates a [PagedList] which can be bound to by a [PagedListAdapter] and also manages the logic of the - * selected media. That includes but not limited to keeping the [GiphyMediaViewModel.selectionNumber] continuous. + * selected media. That includes but not limited to keeping the [GifMediaViewModel.selectionNumber] continuous. * * Calling [setup] is required before using this ViewModel. */ -class GiphyPickerViewModel @Inject constructor( +class GifPickerViewModel @Inject constructor( private val networkUtils: NetworkUtilsWrapper, - private val mediaFetcher: GiphyMediaFetcher, + private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, + private val mediaFetcher: GifMediaFetcher, /** - * The [GiphyPickerDataSourceFactory] to use + * The [GifPickerDataSourceFactory] to use * * This is only available in the constructor to allow mocking in tests. */ - private val dataSourceFactory: GiphyPickerDataSourceFactory + private val dataSourceFactory: GifPickerDataSourceFactory ) : CoroutineScopedViewModel() { /** * A result of [downloadSelected] observed using the [downloadResult] LiveData @@ -95,7 +97,7 @@ class GiphyPickerViewModel @Inject constructor( /** * Errors that happened during page loads. * - * @see [GiphyPickerDataSource.rangeLoadErrorEvent] + * @see [GifPickerDataSource.rangeLoadErrorEvent] */ val rangeLoadErrorEvent: LiveData = dataSourceFactory.rangeLoadErrorEvent @@ -113,13 +115,13 @@ class GiphyPickerViewModel @Inject constructor( */ val downloadResult: LiveData = _downloadResult - private val _selectedMediaViewModelList = MutableLiveData>() + private val _selectedMediaViewModelList = MutableLiveData>() /** - * A [Map] of the [GiphyMediaViewModel]s that were selected by the user + * A [Map] of the [GifMediaViewModel]s that were selected by the user * - * This map is sorted in the order that the user picked them. The [String] is the value of [GiphyMediaViewModel.id]. + * This map is sorted in the order that the user picked them. The [String] is the value of [GifMediaViewModel.id]. */ - val selectedMediaViewModelList: LiveData> = _selectedMediaViewModelList + val selectedMediaViewModelList: LiveData> = _selectedMediaViewModelList /** * Returns `true` if the selection bar (UI) should be shown @@ -140,15 +142,22 @@ class GiphyPickerViewModel @Inject constructor( /** * The [PagedList] that should be displayed in the RecyclerView */ - val mediaViewModelPagedList: LiveData> by lazy { - val pagedListConfig = PagedList.Config.Builder().setEnablePlaceholders(true).setPageSize(30).build() - LivePagedListBuilder(dataSourceFactory, pagedListConfig).setBoundaryCallback(pagedListBoundaryCallback).build() + val mediaViewModelPagedList: LiveData> by lazy { + val pagedListConfig = PagedList.Config.Builder() + .setEnablePlaceholders(false) + .setInitialLoadSizeHint(DEFAULT_INITIAL_LOAD_SIZE_HINT) + .setPageSize(DEFAULT_PAGE_SIZE) + .build() + + LivePagedListBuilder(dataSourceFactory, pagedListConfig) + .setBoundaryCallback(pagedListBoundaryCallback) + .build() } /** * Update the [emptyDisplayMode] depending on the number of API search results or whether there was an error. */ - private val pagedListBoundaryCallback = object : BoundaryCallback() { + private val pagedListBoundaryCallback = object : BoundaryCallback() { override fun onZeroItemsLoaded() { _isPerformingInitialLoad.postValue(false) @@ -161,7 +170,7 @@ class GiphyPickerViewModel @Inject constructor( super.onZeroItemsLoaded() } - override fun onItemAtFrontLoaded(itemAtFront: GiphyMediaViewModel) { + override fun onItemAtFrontLoaded(itemAtFront: GifMediaViewModel) { _isPerformingInitialLoad.postValue(false) _emptyDisplayMode.postValue(EmptyDisplayMode.HIDDEN) super.onItemAtFrontLoaded(itemAtFront) @@ -199,7 +208,7 @@ class GiphyPickerViewModel @Inject constructor( * when, presumably, the user has stopped typing. * * This also clears the [selectedMediaViewModelList]. This makes sense because the user will not be seeing the - * currently selected [GiphyMediaViewModel] if the new search query results are different. + * currently selected [GifMediaViewModel] if the new search query results are different. * * Searching is disabled if downloading or the [query] is the same as the last one. * @@ -224,14 +233,14 @@ class GiphyPickerViewModel @Inject constructor( dataSourceFactory.searchQuery = query - AnalyticsTracker.track(AnalyticsTracker.Stat.GIPHY_PICKER_SEARCHED) + analyticsTrackerWrapper.track(AnalyticsTracker.Stat.GIF_PICKER_SEARCHED) } else { searchQueryChannel.send(query) } } /** - * Downloads all the selected [GiphyMediaViewModel] + * Downloads all the selected [GifMediaViewModel] * * When the process is finished, the results will be posted to [downloadResult]. * @@ -260,8 +269,8 @@ class GiphyPickerViewModel @Inject constructor( _state.postValue(State.DOWNLOADING) val result = try { - val giphyMediaViewModels = _selectedMediaViewModelList.value?.values?.toList() ?: emptyList() - val mediaModels = mediaFetcher.fetchAndSave(giphyMediaViewModels, site) + val gifMediaViewModels = _selectedMediaViewModelList.value?.values?.toList() ?: emptyList() + val mediaModels = mediaFetcher.fetchAndSave(gifMediaViewModels, site) DownloadResult(mediaModels = mediaModels) } catch (e: CancellationException) { // We don't need to handle coroutine cancellations. The UI should just do nothing. @@ -276,17 +285,17 @@ class GiphyPickerViewModel @Inject constructor( } /** - * Toggles a [GiphyMediaViewModel]'s `isSelected` property between true and false + * Toggles a [GifMediaViewModel]'s `isSelected` property between true and false * - * This also updates the [GiphyMediaViewModel.selectionNumber] of all the objects in [selectedMediaViewModelList]. + * This also updates the [GifMediaViewModel.selectionNumber] of all the objects in [selectedMediaViewModelList]. */ - fun toggleSelected(mediaViewModel: GiphyMediaViewModel) { + fun toggleSelected(mediaViewModel: GifMediaViewModel) { if (_state.value != State.IDLE) { return } - assert(mediaViewModel is MutableGiphyMediaViewModel) - mediaViewModel as MutableGiphyMediaViewModel + assert(mediaViewModel is MutableGifMediaViewModel) + mediaViewModel as MutableGifMediaViewModel val isSelected = !(mediaViewModel.isSelected.value ?: false) @@ -308,14 +317,14 @@ class GiphyPickerViewModel @Inject constructor( } /** - * Update the [GiphyMediaViewModel.selectionNumber] values so that they are continuous + * Update the [GifMediaViewModel.selectionNumber] values so that they are continuous * - * For example, if the selection numbers are [1, 2, 3, 4, 5] and the 2nd [GiphyMediaViewModel] was removed, we + * For example, if the selection numbers are [1, 2, 3, 4, 5] and the 2nd [GifMediaViewModel] was removed, we * want the selection numbers to be updated to [1, 2, 3, 4] instead of leaving it as [1, 3, 4, 5]. */ - private fun rebuildSelectionNumbers(mediaList: LinkedHashMap) { + private fun rebuildSelectionNumbers(mediaList: LinkedHashMap) { mediaList.values.forEachIndexed { index, mediaViewModel -> - (mediaViewModel as MutableGiphyMediaViewModel).postSelectionNumber(index + 1) + (mediaViewModel as MutableGifMediaViewModel).postSelectionNumber(index + 1) } } @@ -341,7 +350,12 @@ class GiphyPickerViewModel @Inject constructor( /** * Retries all previously failed page loads. * - * @see [GiphyPickerDataSource.retryAllFailedRangeLoads] + * @see [GifPickerDataSource.retryAllFailedRangeLoads] */ fun retryAllFailedRangeLoads() = dataSourceFactory.retryAllFailedRangeLoads() + + companion object { + private const val DEFAULT_INITIAL_LOAD_SIZE_HINT = 42 + private const val DEFAULT_PAGE_SIZE = 21 + } } diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/MutableGiphyMediaViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/MutableGifMediaViewModel.kt similarity index 73% rename from WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/MutableGiphyMediaViewModel.kt rename to WordPress/src/main/java/org/wordpress/android/viewmodel/gif/MutableGifMediaViewModel.kt index 1b3ef9f9905b..2fee820ad649 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/giphy/MutableGiphyMediaViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/MutableGifMediaViewModel.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.viewmodel.giphy +package org.wordpress.android.viewmodel.gif import android.net.Uri import androidx.lifecycle.LiveData @@ -6,21 +6,21 @@ import androidx.lifecycle.MutableLiveData import org.wordpress.android.viewmodel.SingleLiveEvent /** - * A mutable implementation of [GiphyMediaViewModel] + * A mutable implementation of [GifMediaViewModel] * - * This is meant to be accessible by [GiphyPickerViewModel] and [GiphyPickerDataSource] only. This is designed this - * way so that [GiphyPickerViewModel] encapsulates all the logic of managing selected items as well as keeping their + * This is meant to be accessible by [GifPickerViewModel] and [GifPickerDataSource] only. This is designed this + * way so that [GifPickerViewModel] encapsulates all the logic of managing selected items as well as keeping their * selection numbers continuous. * - * The [GiphyPickerViewHolder] should never have access to the mutating methods of this class. + * The [GifPickerViewHolder] should never have access to the mutating methods of this class. */ -class MutableGiphyMediaViewModel( +data class MutableGifMediaViewModel( override val id: String, override val thumbnailUri: Uri, override val previewImageUri: Uri, override val largeImageUri: Uri, override val title: String -) : GiphyMediaViewModel { +) : GifMediaViewModel { /** * Using [SingleLiveEvent] will prevent calls like this from running immediately when a ViewHolder is bound: * diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/provider/GifProvider.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/provider/GifProvider.kt new file mode 100644 index 000000000000..0aee13220740 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/provider/GifProvider.kt @@ -0,0 +1,33 @@ +package org.wordpress.android.viewmodel.gif.provider + +import org.wordpress.android.viewmodel.gif.GifMediaViewModel + +/** + * Interface to interact with a GIF provider API avoiding coupling with the concrete implementation + */ +interface GifProvider { + /** + * Request GIF search from a query string + * + * @param query represents the desired text to search within the provider. + * @param position to request results starting from a given position for that query + * @param loadSize to request a result list limited to a specific amount + * @param onSuccess will be called if the Provider had success finding GIFs + * and will deliver a [List] of [GifMediaViewModel] with all matching GIFs + * and a [Int] describing the next position for pagination + * @param onFailure will be called if the Provider didn't succeed with the task of bringing GIFs, + * the delivered String will describe the error to be presented to the user + */ + fun search( + query: String, + position: Int, + loadSize: Int? = null, + onSuccess: (List, Int?) -> Unit, + onFailure: (Throwable) -> Unit + ) + + /** + * An Exception to describe errors within the Provider when a [onFailure] is called + */ + class GifRequestFailedException(message: String) : Exception(message) +} diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/provider/TenorProvider.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/provider/TenorProvider.kt new file mode 100644 index 000000000000..de6c56718267 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/gif/provider/TenorProvider.kt @@ -0,0 +1,150 @@ +package org.wordpress.android.viewmodel.gif.provider + +import android.content.Context +import android.net.Uri +import com.tenor.android.core.constant.AspectRatioRange +import com.tenor.android.core.constant.MediaCollectionFormat +import com.tenor.android.core.constant.MediaFilter +import com.tenor.android.core.model.impl.Result +import com.tenor.android.core.network.ApiClient +import com.tenor.android.core.network.IApiClient +import com.tenor.android.core.response.impl.GifsResponse +import org.wordpress.android.R +import org.wordpress.android.viewmodel.gif.GifMediaViewModel +import org.wordpress.android.viewmodel.gif.MutableGifMediaViewModel +import org.wordpress.android.viewmodel.gif.provider.GifProvider.GifRequestFailedException +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +/** + * Implementation of a GifProvider using the Tenor GIF API as provider + * + * This Provider performs requests to the Tenor API using the [tenorClient]. + */ + +internal class TenorProvider constructor( + val context: Context, + private val tenorClient: IApiClient +) : GifProvider { + /** + * Implementation of the [GifProvider] search method, it will call the Tenor client search + * right away with the provided parameters. + * + * If the search request succeeds an [List] of [MutableGifMediaViewModel] will be passed with the next position + * for pagination. If there's no next position provided, it will be passed as null. + * + * If the search request fails an [GifRequestFailedException] will be passed with the API + * message. If there's no message provided, a generic message will be applied. + * + */ + override fun search( + query: String, + position: Int, + loadSize: Int?, + onSuccess: (List, Int?) -> Unit, + onFailure: (Throwable) -> Unit + ) { + tenorClient.enqueueSearchRequest( + query, + position.toString(), + loadSize, + onSuccess = { response -> + val gifList = response.results.map { it.toMutableGifMediaViewModel() } + val nextPosition = response.next.toIntOrNull() + onSuccess(gifList, nextPosition) + }, + onFailure = { + val errorMessage = it?.message + ?: context.getString(R.string.gif_list_search_returned_unknown_error) + onFailure(GifRequestFailedException(errorMessage)) + } + ) + } + + /** + * The [onSuccess] will be called if the response is not null + * + * The [onFailure] will be called assuming that no valid GIF was found + * or that a direct failure was found by Tenor API + * + * Method is inlined for better high-order functions performance + */ + private inline fun IApiClient.enqueueSearchRequest( + query: String, + position: String, + loadSize: Int?, + crossinline onSuccess: (GifsResponse) -> Unit, + crossinline onFailure: (Throwable?) -> Unit + ) = buildSearchCall(query, loadSize, position).apply { + enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val errorMessage = context.getString(R.string.gif_picker_empty_search_list) + response.body()?.let(onSuccess) ?: onFailure(GifRequestFailedException(errorMessage)) + } + + override fun onFailure(call: Call, throwable: Throwable) { + onFailure(throwable) + } + }) + } + + /** + * This method act as a simplification to call a search within the Tenor API and the callback + * creation + * + * [MediaFilter] must be BASIC or the returned Media will not have a displayable thumbnail + * All other provided parameters are set following the Tenor API Documentation + */ + private fun IApiClient.buildSearchCall( + query: String, + loadSize: Int?, + position: String + ) = search( + ApiClient.getServiceIds(context), + query, + loadSize.fittedToMaximumAllowed, + position, + MediaFilter.BASIC, + AspectRatioRange.ALL + ) + + /** + * Every GIF returned by the Tenor will be available as [Result], to better interface + * with our app, it will be converted to [MutableGifMediaViewModel] to avoid any external + * coupling with the Tenor API + * + * See the [Tenor API docs](https://tenor.com/gifapi/documentation#responseobjects) for more information on what a [Result] object contains. + */ + private fun Result.toMutableGifMediaViewModel() = MutableGifMediaViewModel( + id, + Uri.parse(urlFromCollectionFormat(MediaCollectionFormat.GIF_NANO)), + Uri.parse(urlFromCollectionFormat(MediaCollectionFormat.GIF_TINY)), + Uri.parse(urlFromCollectionFormat(MediaCollectionFormat.GIF)), + title + ) + + private fun Result.urlFromCollectionFormat(format: String) = + medias.firstOrNull()?.get(format)?.url + + /** + * Since the Tenor only allows a maximum of 50 GIFs per request, the API will throw + * an exception if this rule is disrespected, in order to still be resilient in + * provide the desired search, the loadSize will be reduced to the maximum allowed + * if needed + */ + private val Int?.fittedToMaximumAllowed + get() = this?.let { + when { + this > MAXIMUM_ALLOWED_LOAD_SIZE -> MAXIMUM_ALLOWED_LOAD_SIZE + else -> this + } + } ?: MAXIMUM_ALLOWED_LOAD_SIZE + + companion object { + /** + * To better refers to the Tenor API maximum GIF limit per request + */ + private const val MAXIMUM_ALLOWED_LOAD_SIZE = 50 + } +} diff --git a/WordPress/src/main/res/drawable/img_giphy_100dp.xml b/WordPress/src/main/res/drawable/img_giphy_100dp.xml deleted file mode 100644 index 7648c59bbe73..000000000000 --- a/WordPress/src/main/res/drawable/img_giphy_100dp.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/src/main/res/drawable/img_tenor_100dp.xml b/WordPress/src/main/res/drawable/img_tenor_100dp.xml new file mode 100644 index 000000000000..fa9d35cedb2e --- /dev/null +++ b/WordPress/src/main/res/drawable/img_tenor_100dp.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/values-ar/strings.xml b/WordPress/src/main/res/values-ar/strings.xml index 0205ca8a18e0..34946cd49933 100644 --- a/WordPress/src/main/res/values-ar/strings.xml +++ b/WordPress/src/main/res/values-ar/strings.xml @@ -614,11 +614,6 @@ Language: ar لا توجد بيانات لهذه الفترة إزالة موقع من الوسائط يتعذر علينا فتح الإحصاءات في الوقت الحالي. يرجى المحاولة مجددًا في وقت لاحق - مدعون من GIPHY - فشل تحميل بعض الوسائط بسبب وجود خطأ في الشبكة. - لا توجد وسائط مطابقة لبحثك - ابحث للعثور على صور بتنسيق GIF لإضافتها إلى مكتبة الوسائط الخاصة بك! - البحث في Giphy مشاهدات الكاتب الكُتّاب diff --git a/WordPress/src/main/res/values-el/strings.xml b/WordPress/src/main/res/values-el/strings.xml index fb46d670dbf4..358c66dcce2c 100644 --- a/WordPress/src/main/res/values-el/strings.xml +++ b/WordPress/src/main/res/values-el/strings.xml @@ -55,7 +55,6 @@ Language: el_GR %1$d από %2$d Δημιουργία ιστότοπου Αναίρεση - Αναζήτηση στο Giphy Προβολές Συγγραφέας Συγγραφείς diff --git a/WordPress/src/main/res/values-en-rAU/strings.xml b/WordPress/src/main/res/values-en-rAU/strings.xml index 889aeda5aa51..1ae880a91998 100644 --- a/WordPress/src/main/res/values-en-rAU/strings.xml +++ b/WordPress/src/main/res/values-en-rAU/strings.xml @@ -378,11 +378,6 @@ Language: en_AU No data for this period Remove location from media We cannot open the statistics at the moment. Please try again later - Powered by GIPHY - Some media failed to load due to a network error. - No media matching your search - Search to find GIFs to add to your Media Library! - Search Giphy Views Author Authors diff --git a/WordPress/src/main/res/values-en-rCA/strings.xml b/WordPress/src/main/res/values-en-rCA/strings.xml index 12b4c2d12dcd..47551845a740 100644 --- a/WordPress/src/main/res/values-en-rCA/strings.xml +++ b/WordPress/src/main/res/values-en-rCA/strings.xml @@ -473,11 +473,6 @@ Language: en_CA No data for this period Remove location from media We cannot open the statistics at the moment. Please try again later - Powered by GIPHY - Some media failed to load due to a network error. - No media matching your search - Search to find GIFs to add to your Media Library! - Search Giphy Views Author Authors diff --git a/WordPress/src/main/res/values-en-rGB/strings.xml b/WordPress/src/main/res/values-en-rGB/strings.xml index a004ce89206e..a77945f3ee2c 100644 --- a/WordPress/src/main/res/values-en-rGB/strings.xml +++ b/WordPress/src/main/res/values-en-rGB/strings.xml @@ -614,11 +614,6 @@ Language: en_GB No data for this period Remove location from media We cannot open the statistics at the moment. Please try again later - Powered by GIPHY - Some media failed to load due to a network error. - No media matching your search - Search to find GIFs to add to your Media Library! - Search Giphy Views Author Authors diff --git a/WordPress/src/main/res/values-es-rCL/strings.xml b/WordPress/src/main/res/values-es-rCL/strings.xml index b552e7dd122d..12f3d8331b9a 100644 --- a/WordPress/src/main/res/values-es-rCL/strings.xml +++ b/WordPress/src/main/res/values-es-rCL/strings.xml @@ -141,11 +141,6 @@ Language: es_CL No hay datos para este período Quita la ubicación de los medios No podemos abrir las estadísticas en este momento. Por favor inténtalo de nuevo más tarde - Desarrollado por GIPHY - Algunos medios no pudieron cargarse debido a un error de red. - No hay medios que coincidan con tu búsqueda - ¡Busca para encontrar GIFs para añadirlos a tu Biblioteca de Medios! - Buscar en Giphy Vistas Autor Autores diff --git a/WordPress/src/main/res/values-es-rMX/strings.xml b/WordPress/src/main/res/values-es-rMX/strings.xml index a1a30fa3f85a..30e67c3e1442 100644 --- a/WordPress/src/main/res/values-es-rMX/strings.xml +++ b/WordPress/src/main/res/values-es-rMX/strings.xml @@ -614,11 +614,6 @@ Language: es_MX No hay datos en este periodo Eliminar la ubicación de los medios No podemos abrir las estadísticas en este momento. Por favor inténtalo de nuevo más tarde - Desarrollado por GIPHY - Algunos medios no se pudieron cargar debido a un error de red. - No hay medios que coincidan con tu búsqueda. - ¡Busca para encontrar GIFs para agregar a tu biblioteca de medios! - Buscar en Giphy Vistas Autor Autores diff --git a/WordPress/src/main/res/values-es-rVE/strings.xml b/WordPress/src/main/res/values-es-rVE/strings.xml index 235e0f516aaa..631d80afe381 100644 --- a/WordPress/src/main/res/values-es-rVE/strings.xml +++ b/WordPress/src/main/res/values-es-rVE/strings.xml @@ -614,11 +614,6 @@ Language: es_VE No hay datos en este periodo Eliminar la ubicación de los medios No podemos abrir las estadísticas en este momento. Por favor, inténtalo de nuevo más tarde - Creado por GIPHY - Algunos medios no se cargaron debido a un error de red. - Ningún medio coincide con tu búsqueda - ¡Busca para encontrar GIFs para añadir a tu biblioteca de medios! - Buscar en Giphy Vistas Autor Autores diff --git a/WordPress/src/main/res/values-he/strings.xml b/WordPress/src/main/res/values-he/strings.xml index 362a3859f0cd..ba4e1f233de8 100644 --- a/WordPress/src/main/res/values-he/strings.xml +++ b/WordPress/src/main/res/values-he/strings.xml @@ -557,11 +557,6 @@ Language: he_IL אין נתונים לתקופה זו הסרת מיקום ממדיה אין לנו אפשרות לפתוח את הנתונים הסטטיסטיים כעת. יש לנסות שוב מאוחר יותר - מופעל באמצעות GIPHY - הטעינה של חלק מקובצי המדיה נכשלה עקב שגיאת רשת. - לא נמצאו פרטי מדיה שמתאימים לחיפוש שלך - חיפוש קובצי GIF שניתן להוסיף לספריית מדיה שלך! - חיפוש ב-Giphy צפיות מחבר מחברים diff --git a/WordPress/src/main/res/values-id/strings.xml b/WordPress/src/main/res/values-id/strings.xml index 8fffd4d2887e..8de1d426cf2b 100644 --- a/WordPress/src/main/res/values-id/strings.xml +++ b/WordPress/src/main/res/values-id/strings.xml @@ -554,11 +554,6 @@ Language: id Tidak ada data selama periode ini Hapus lokasi dari media Saat ini kami tidak dapat membuka statistik. Harap coba lagi nanti - Didukung oleh GIPHY - Sebagian media gagal dimuat karena terdapat error jaringan. - Tidak ada media yang cocok dengan pencarian Anda - Cari GIF untuk ditambahkan ke Pustaka Media Anda! - Cari di Giphy Tampilan Penulis Penulis diff --git a/WordPress/src/main/res/values-it/strings.xml b/WordPress/src/main/res/values-it/strings.xml index c10f3a8703ad..0e624805d8fe 100644 --- a/WordPress/src/main/res/values-it/strings.xml +++ b/WordPress/src/main/res/values-it/strings.xml @@ -542,11 +542,6 @@ Language: it Nessun dato per questo periodo Rimuovi posizione dal contenuto multimediale Non è possibile aprire le statistiche. Riprova più tardi - Powered by GIPHY - Alcuni caricamenti multimediali non sono riusciti a causa di un errore di rete. - Nessun media corrispondente alla tua ricerca - Cerca per trovare GIF da aggiungere alla tua Libreria multimediale! - Cerca in Giphy Visualizzazioni Autore Autori diff --git a/WordPress/src/main/res/values-ja/strings.xml b/WordPress/src/main/res/values-ja/strings.xml index 707691413ae8..66413c7053a2 100644 --- a/WordPress/src/main/res/values-ja/strings.xml +++ b/WordPress/src/main/res/values-ja/strings.xml @@ -558,11 +558,6 @@ Language: ja_JP この期間のデータがありません メディアから位置情報を削除する 現在統計を開けません。後ほど、もう一度お試しください - Powered by GIPHY - ネットワークエラーにより一部のメディアが読み込めませんでした。 - 検索と一致するメディアがありません - GIF を検索して、メディアライブラリに追加してください。 - Giphy を検索する 表示回数 投稿者 投稿者 diff --git a/WordPress/src/main/res/values-kmr/strings.xml b/WordPress/src/main/res/values-kmr/strings.xml index 19f0a4ed4a88..c7095da7f11d 100644 --- a/WordPress/src/main/res/values-kmr/strings.xml +++ b/WordPress/src/main/res/values-kmr/strings.xml @@ -302,11 +302,6 @@ Language: ku_TR Ji bo vê heyamê dane tune Ji medyayê cih rake Rêjejimar vêga nayên vekirin. Paştre dîsa biceribîne. - Bi piştevaniya GIPHYê - Ji ber çewtiya girêdanê barkirina hinek medyayan biserneket. - Lêgerîna te bi ti medyayan re hevber nebû - Ji bo tu GIFan tevlî PirtukxaneyaMedyayê bike, bigere! - Li ser Giphyê bigere Dîtin Nivîskar Nivîskar diff --git a/WordPress/src/main/res/values-ko/strings.xml b/WordPress/src/main/res/values-ko/strings.xml index aabe30ad038b..471e218801fc 100644 --- a/WordPress/src/main/res/values-ko/strings.xml +++ b/WordPress/src/main/res/values-ko/strings.xml @@ -553,11 +553,6 @@ Language: ko_KR 이 기간에 데이터 없음 미디어에서 위치 제거 지금은 통계를 열 수 없습니다. 나중에 다시 시도해 주세요. - GIPHY 제공 - 네트워크 오류로 인해 일부 미디어를 로드하지 못했습니다. - 검색과 일치하는 미디어 없음 - GIF를 검색하여 미디어 라이브러리에 추가하세요! - Giphy 검색 조회수 글쓴이 글쓴이 diff --git a/WordPress/src/main/res/values-nb/strings.xml b/WordPress/src/main/res/values-nb/strings.xml index 12a0a0d4a5a7..21da82b50980 100644 --- a/WordPress/src/main/res/values-nb/strings.xml +++ b/WordPress/src/main/res/values-nb/strings.xml @@ -530,11 +530,6 @@ Language: nb_NO Ingen data for denne perioden Fjern dette stedet fra media Vi kan ikke åpne statistikken i øyeblikket. Vennligst prøv igjen senere - Drevet av GIPHY - Noe media kunne ikke lastes på grunn av en nettverksfeil. - Ingen media passet ditt søk - Søk for å finne GIF-er å legge til i ditt mediebibliotek! - Søk Giphy Visninger Forfatter Forfattere diff --git a/WordPress/src/main/res/values-nl/strings.xml b/WordPress/src/main/res/values-nl/strings.xml index c1679f5819a3..06952a24e903 100644 --- a/WordPress/src/main/res/values-nl/strings.xml +++ b/WordPress/src/main/res/values-nl/strings.xml @@ -512,8 +512,6 @@ Language: nl Geen gegevens voor deze periode Locatie van media verwijderen We kunnen de statistieken momenteel niet openen. Probeer het later opnieuw - Mogelijk gemaakt door GIPHY - Zoek op Giphy Auteur Auteurs Zoekterm diff --git a/WordPress/src/main/res/values-pl/strings.xml b/WordPress/src/main/res/values-pl/strings.xml index 2747cd90c004..f24f07bd4b4a 100644 --- a/WordPress/src/main/res/values-pl/strings.xml +++ b/WordPress/src/main/res/values-pl/strings.xml @@ -610,11 +610,6 @@ Language: pl Brak danych dla tego okresu Usuń lokalizację GPS z plików mediów Nie możemy w tym momencie otworzyć statystyk. Spróbuj ponownie później - Wspierane przez GIPHY - Niektórych plików mediów nie udało się przesłać z powodu błędu sieci. - Brak plików mediów dopasowanych do twojego zapytania - Przeszukaj aby znaleźć pliki GIF które możesz dodać do swojej biblioteki mediów! - Przeszukaj Giphy Wyświetleń Autor Autorzy diff --git a/WordPress/src/main/res/values-pt-rBR/strings.xml b/WordPress/src/main/res/values-pt-rBR/strings.xml index 0785ed1501a7..4fa08504a000 100644 --- a/WordPress/src/main/res/values-pt-rBR/strings.xml +++ b/WordPress/src/main/res/values-pt-rBR/strings.xml @@ -559,11 +559,6 @@ Language: pt_BR Nenhum dado para esse período Remover localização de mídia Não conseguimos abrir as estatísticas agora. Tente novamente mais tarde. - Oferecido pelo GIPHY - Alguns arquivos não puderam ser carregados devido a problemas com a conexão. - Nenhuma mídia corresponde à sua pesquisa - Pesquise para encontrar gifs para adicionar à sua biblioteca de mídias. - Pesquisar no Giphy Visualizações Autor Autores diff --git a/WordPress/src/main/res/values-ro/strings.xml b/WordPress/src/main/res/values-ro/strings.xml index 84c0eb15525a..41bef6b1b2e9 100644 --- a/WordPress/src/main/res/values-ro/strings.xml +++ b/WordPress/src/main/res/values-ro/strings.xml @@ -614,11 +614,6 @@ Language: ro Nu există date pentru această perioadă Înlătură locația din Media Pentru moment, nu putem deschide statisticile. Te rog reîncearcă mai târziu - Propulsat de GIPHY - Unele elemente media au eșuat la încărcare din cauza unei erori de rețea. - Nu s-a potrivit niciun element media cu căutarea ta - Caută pentru a găsi imagini GIF pe care să le adaugi în Biblioteca ta media! - Caută cu Giphy Vizualizări Autor Autori diff --git a/WordPress/src/main/res/values-ru/strings.xml b/WordPress/src/main/res/values-ru/strings.xml index ee6a709df17f..dab84cf0079b 100644 --- a/WordPress/src/main/res/values-ru/strings.xml +++ b/WordPress/src/main/res/values-ru/strings.xml @@ -614,11 +614,6 @@ Language: ru Для указанного периода нет данных Убрать данные местоположения из медиафайлов Извините, статистика в данный момент недоступна, попробуйте позже. - Работает с GIPHY - Некоторые медиафайлы не загрузились из-за ошибки сети. - Мультимедиа по критериям поиска не найдены - Найдите GIF изображения и добавьте их в медиатеку! - Поиск в Giphy Просмотры Автор Авторы diff --git a/WordPress/src/main/res/values-sk/strings.xml b/WordPress/src/main/res/values-sk/strings.xml index 463ffda16d17..a3e156cfd385 100644 --- a/WordPress/src/main/res/values-sk/strings.xml +++ b/WordPress/src/main/res/values-sk/strings.xml @@ -44,8 +44,6 @@ Language: sk Vrátiť späť Žiadne dáta pre toto obdobie Odstrániť lokáciu z médiá - Poháňa GIPHY - Vyhľadať v Giphy Zobrazenia Autor Autori diff --git a/WordPress/src/main/res/values-sq/strings.xml b/WordPress/src/main/res/values-sq/strings.xml index 2a2e77a1f011..6e5b160f3be8 100644 --- a/WordPress/src/main/res/values-sq/strings.xml +++ b/WordPress/src/main/res/values-sq/strings.xml @@ -614,11 +614,6 @@ Language: sq_AL S’ka të dhëna për këtë periudhë Hiqe vendndodhjen prej mediash S’i hapim dot statistikat tani. Ju lutemi, riprovoni më vonë - Bazuar në Giphy - Dështoi ngarkimi për ca media, për shkak të një gabimi rrjeti. - S’ka media që përputhet me kërkimin tuaj - Kërkoni për të gjetur GIF-e që t’i shtoni te Mediateka juaj! - Kërkoni në Giphy Parje Autor Autorë diff --git a/WordPress/src/main/res/values-sv/strings.xml b/WordPress/src/main/res/values-sv/strings.xml index 5570c93ee2eb..232991df40b7 100644 --- a/WordPress/src/main/res/values-sv/strings.xml +++ b/WordPress/src/main/res/values-sv/strings.xml @@ -614,11 +614,6 @@ Language: sv_SE Inga uppgifter för denna tidsperiod Avlägsna position från media Just nu går det inte att öppna statistiken. Försök igen senare - Drivs med hjälp av GIPHY - Vissa mediefiler kunde inte laddas på grund av nätverksproblem. - Inga media matchar din sökning - Leta efter GIF-filer du kan lägga till i ditt mediebibliotek! - Sök i Giphy Visningar Författare Författare diff --git a/WordPress/src/main/res/values-tr/strings.xml b/WordPress/src/main/res/values-tr/strings.xml index a742c0010d85..b84be1ff01b5 100644 --- a/WordPress/src/main/res/values-tr/strings.xml +++ b/WordPress/src/main/res/values-tr/strings.xml @@ -559,11 +559,6 @@ Language: tr Bu dönem için veri yok Ortam dosyasından konum bilgisini kaldır Şu anda istatistikleri açamıyoruz. Lütfen daha sonra tekrar deneyiniz. - GIPHY tarafından sağlanır - Ağ hatası nedeniyle bazı ortam dosyaları yüklenemedi. - Aramanızla eşleşen bir ortam yok - Ortam kütüphanenize GIF bulup eklemek için arayın! - Giphy\'de ara Görüntülemeler Yazar Yazarlar diff --git a/WordPress/src/main/res/values-zh-rCN/strings.xml b/WordPress/src/main/res/values-zh-rCN/strings.xml index 6ebbec25ac04..998d5617ec3c 100644 --- a/WordPress/src/main/res/values-zh-rCN/strings.xml +++ b/WordPress/src/main/res/values-zh-rCN/strings.xml @@ -502,11 +502,6 @@ Language: zh_CN 没有此时间段的数据 从媒体删除位置信息 我们现在无法打开统计信息。请稍后重试 - 由 GIPHY 提供支持 - 由于网络错误,无法加载某些媒体。 - 没有与您的搜索匹配的媒体 - 搜索查找要添加到您的媒体库中的 GIF! - 搜索 Giphy 浏览量 作者 作者 diff --git a/WordPress/src/main/res/values-zh-rHK/strings.xml b/WordPress/src/main/res/values-zh-rHK/strings.xml index 23e9cc316b71..097b6a04c581 100644 --- a/WordPress/src/main/res/values-zh-rHK/strings.xml +++ b/WordPress/src/main/res/values-zh-rHK/strings.xml @@ -549,11 +549,6 @@ Language: zh_TW 此期間沒有資料 從媒體移除位置資訊 目前我們無法開啟統計資料。請稍後再試一次 - 由 GIPHY 建置 - 網路發生錯誤,導致部份媒體載入失敗。 - 沒有符合你搜尋條件的媒體 - 搜尋免費 GIF 以新增至你的媒體庫! - 搜尋 Giphy 瀏覽數 作者 作者 diff --git a/WordPress/src/main/res/values-zh-rTW/strings.xml b/WordPress/src/main/res/values-zh-rTW/strings.xml index 23e9cc316b71..097b6a04c581 100644 --- a/WordPress/src/main/res/values-zh-rTW/strings.xml +++ b/WordPress/src/main/res/values-zh-rTW/strings.xml @@ -549,11 +549,6 @@ Language: zh_TW 此期間沒有資料 從媒體移除位置資訊 目前我們無法開啟統計資料。請稍後再試一次 - 由 GIPHY 建置 - 網路發生錯誤,導致部份媒體載入失敗。 - 沒有符合你搜尋條件的媒體 - 搜尋免費 GIF 以新增至你的媒體庫! - 搜尋 Giphy 瀏覽數 作者 作者 diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index c5067d92b343..3e59d06a4e75 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -2240,6 +2240,7 @@ Take photo Take video Choose from Free Photo Library + Choose from Tenor %s needs permissions to access your photos Allow %1$s was denied access to your photos. To fix this, edit your permissions and turn on %2$s. @@ -2253,11 +2254,13 @@ Search to find free photos to add to your Media Library Photos provided by %s - Search Giphy - Search to find GIFs to add to your Media Library! - No media matching your search - Some media failed to load due to a network error. - Powered by GIPHY + + Search Tenor + Powered by Tenor + Search to find GIFs to add to your Media Library! + Some media failed to load due to a network error. + No media matching your search + There was a problem handling the request Saving post as draft @@ -2658,7 +2661,6 @@ Search posts No posts matching your search - Default Desktop diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/media/EditorMediaTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/media/EditorMediaTest.kt index ea896b5e7cce..037177a2fa19 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/media/EditorMediaTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/media/EditorMediaTest.kt @@ -121,7 +121,7 @@ class EditorMediaTest : BaseUnitTest() { } @Test - fun `addMediaFromGiphyToPostAsync invokes addLocalMediaToEditorAsync with all media`() = test { + fun `addGifMediaToPostAsync invokes addLocalMediaToEditorAsync with all media`() = test { // Arrange val localIdArray = listOf(1, 2, 3).toIntArray() val addLocalMediaToPostUseCase = createAddLocalMediaToPostUseCase() @@ -130,7 +130,7 @@ class EditorMediaTest : BaseUnitTest() { createEditorMedia( addLocalMediaToPostUseCase = addLocalMediaToPostUseCase ) - .addMediaFromGiphyToPostAsync(localIdArray) + .addGifMediaToPostAsync(localIdArray) // Assert verify(addLocalMediaToPostUseCase).addLocalMediaToEditorAsync( eq(localIdArray.toList()), diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/gif/GifPickerDataSourceFixtures.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/gif/GifPickerDataSourceFixtures.kt new file mode 100644 index 000000000000..0dd6848c5049 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/gif/GifPickerDataSourceFixtures.kt @@ -0,0 +1,12 @@ +package org.wordpress.android.viewmodel.gif + +import com.nhaarman.mockitokotlin2.mock + +object GifPickerDataSourceFixtures { + internal val expectedGifMediaViewModelCollection: List = listOf( + mock(), + mock(), + mock(), + mock() + ) +} diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/gif/GifPickerDataSourceTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/gif/GifPickerDataSourceTest.kt new file mode 100644 index 000000000000..cda0efc1d200 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/gif/GifPickerDataSourceTest.kt @@ -0,0 +1,228 @@ +package org.wordpress.android.viewmodel.gif + +import androidx.paging.PositionalDataSource.LoadInitialCallback +import androidx.paging.PositionalDataSource.LoadInitialParams +import androidx.paging.PositionalDataSource.LoadRangeCallback +import androidx.paging.PositionalDataSource.LoadRangeParams +import com.nhaarman.mockitokotlin2.KArgumentCaptor +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.argumentCaptor +import com.nhaarman.mockitokotlin2.spy +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verify +import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.wordpress.android.viewmodel.gif.GifPickerDataSourceFixtures.expectedGifMediaViewModelCollection +import org.wordpress.android.viewmodel.gif.provider.GifProvider +import org.wordpress.android.viewmodel.gif.provider.GifProvider.GifRequestFailedException + +class GifPickerDataSourceTest { + @Mock lateinit var gifProvider: GifProvider + + lateinit var onSuccessCaptor: KArgumentCaptor<(List, Int?) -> Unit> + + lateinit var onFailureCaptor: KArgumentCaptor<(Throwable) -> Unit> + + private lateinit var pickerDataSourceUnderTest: GifPickerDataSource + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + pickerDataSourceUnderTest = GifPickerDataSource(gifProvider, "test") + onSuccessCaptor = argumentCaptor() + onFailureCaptor = argumentCaptor() + } + + @Test + fun `loadInitial should call onResult when search is successful`() { + var onResultWasCalled = false + + val params = LoadInitialParams(0, 20, 5, false) + val dataSourceCallback = object : LoadInitialCallback() { + override fun onResult(data: MutableList, position: Int, totalCount: Int) { + onResultWasCalled = true + assertThat(data).isEqualTo(expectedGifMediaViewModelCollection) + assertThat(position).isEqualTo(0) + assertThat(totalCount).isEqualTo(4) + } + + override fun onResult(data: MutableList, position: Int) { + fail("Wrong onResult called") + } + } + pickerDataSourceUnderTest.loadInitial(params, dataSourceCallback) + + verify(gifProvider, times(1)).search(any(), any(), any(), onSuccessCaptor.capture(), any()) + val capturedProviderOnSuccess = onSuccessCaptor.firstValue + capturedProviderOnSuccess(expectedGifMediaViewModelCollection, 4) + assertThat(onResultWasCalled).isTrue() + } + + @Test + fun `loadInitial should call onResult with correct position when search is successful but next position is null`() { + var onResultWasCalled = false + + val params = LoadInitialParams(0, 20, 5, false) + val dataSourceCallback = object : LoadInitialCallback() { + override fun onResult(data: MutableList, position: Int, totalCount: Int) { + onResultWasCalled = true + assertThat(data).isEqualTo(expectedGifMediaViewModelCollection) + assertThat(position).isEqualTo(0) + assertThat(totalCount).isEqualTo(4) + } + + override fun onResult(data: MutableList, position: Int) { + fail("Wrong onResult called") + } + } + pickerDataSourceUnderTest.loadInitial(params, dataSourceCallback) + + verify(gifProvider, times(1)).search(any(), any(), any(), onSuccessCaptor.capture(), any()) + val capturedProviderOnSuccess = onSuccessCaptor.firstValue + capturedProviderOnSuccess(expectedGifMediaViewModelCollection, null) + assertThat(onResultWasCalled).isTrue() + } + + @Test + fun `loadInitial should call onResult with position when search is successful but next position is incorrect`() { + var onResultWasCalled = false + + val params = LoadInitialParams(0, 20, 5, false) + val dataSourceCallback = object : LoadInitialCallback() { + override fun onResult(data: MutableList, position: Int, totalCount: Int) { + onResultWasCalled = true + assertThat(data).isEqualTo(expectedGifMediaViewModelCollection) + assertThat(position).isEqualTo(0) + assertThat(totalCount).isEqualTo(4) + } + + override fun onResult(data: MutableList, position: Int) { + fail("Wrong onResult called") + } + } + pickerDataSourceUnderTest.loadInitial(params, dataSourceCallback) + + verify(gifProvider, times(1)).search(any(), any(), any(), onSuccessCaptor.capture(), any()) + val capturedProviderOnSuccess = onSuccessCaptor.firstValue + capturedProviderOnSuccess(expectedGifMediaViewModelCollection, 2) + assertThat(onResultWasCalled).isTrue() + } + + @Test + fun `loadInitial should call onResult with emptyList when search query is blank`() { + var onResultWasCalled = false + pickerDataSourceUnderTest = GifPickerDataSource(gifProvider, "") + + val params = LoadInitialParams(0, 20, 5, false) + val dataSourceCallback = object : LoadInitialCallback() { + override fun onResult(data: MutableList, position: Int, totalCount: Int) { + onResultWasCalled = true + assertThat(data).isEmpty() + assertThat(position).isEqualTo(0) + assertThat(totalCount).isEqualTo(0) + } + + override fun onResult(data: MutableList, position: Int) { + fail("Wrong onResult called") + } + } + pickerDataSourceUnderTest.loadInitial(params, dataSourceCallback) + + verify(gifProvider, times(0)).search(any(), any(), any(), any(), any()) + assertThat(onResultWasCalled).isTrue() + } + + @Test + fun `loadInitial should call onResult when search fails with emptyList`() { + var onResultWasCalled = false + val expectedThrowable = GifRequestFailedException("Test throwable") + + val params = LoadInitialParams(0, 20, 5, false) + val dataSourceCallback = object : LoadInitialCallback() { + override fun onResult(data: MutableList, position: Int, totalCount: Int) { + onResultWasCalled = true + assertThat(data).isEmpty() + assertThat(position).isEqualTo(0) + assertThat(totalCount).isEqualTo(0) + } + + override fun onResult(data: MutableList, position: Int) { + fail("Wrong onResult called") + } + } + pickerDataSourceUnderTest.loadInitial(params, dataSourceCallback) + + verify(gifProvider, times(1)).search(any(), any(), any(), any(), onFailureCaptor.capture()) + val capturedProviderOnFailure = onFailureCaptor.firstValue + capturedProviderOnFailure(expectedThrowable) + + assertThat(onResultWasCalled).isTrue() + } + + @Test + fun `loadInitial should set initialLoadError when search fails`() { + var onResultWasCalled = false + val expectedThrowable = GifRequestFailedException("Test throwable") + + val params = LoadInitialParams(0, 20, 5, false) + val dataSourceCallback = object : LoadInitialCallback() { + override fun onResult(data: MutableList, position: Int, totalCount: Int) { + onResultWasCalled = true + } + + override fun onResult(data: MutableList, position: Int) { + fail("Wrong onResult called") + } + } + pickerDataSourceUnderTest.loadInitial(params, dataSourceCallback) + + verify(gifProvider, times(1)).search(any(), any(), any(), any(), onFailureCaptor.capture()) + val capturedProviderOnFailure = onFailureCaptor.firstValue + capturedProviderOnFailure(expectedThrowable) + + assertThat(onResultWasCalled).isTrue() + assertThat(pickerDataSourceUnderTest.initialLoadError).isNotNull() + assertThat(pickerDataSourceUnderTest.initialLoadError).isInstanceOf(GifRequestFailedException::class.java) + assertThat(pickerDataSourceUnderTest.initialLoadError?.message).isEqualTo("Test throwable") + } + + @Test + fun `loadRange should call onResult when search is successful`() { + var onResultWasCalled = false + + val params = LoadRangeParams(0, 20) + val dataSourceCallback = object : LoadRangeCallback() { + override fun onResult(data: MutableList) { + onResultWasCalled = true + assertThat(data).isEqualTo(expectedGifMediaViewModelCollection) + } + } + pickerDataSourceUnderTest.loadRange(params, dataSourceCallback) + + verify(gifProvider, times(1)).search(any(), any(), any(), onSuccessCaptor.capture(), any()) + val capturedProviderOnSuccess = onSuccessCaptor.firstValue + capturedProviderOnSuccess(expectedGifMediaViewModelCollection, 4) + assertThat(onResultWasCalled).isTrue() + } + + @Test + fun `loadRange should call retryAllFailedRangeLoads when search is successful`() { + val spiedDataSource = spy(pickerDataSourceUnderTest) + + val params = LoadRangeParams(0, 20) + val dataSourceCallback = object : LoadRangeCallback() { + override fun onResult(data: MutableList) {} + } + spiedDataSource.loadRange(params, dataSourceCallback) + + verify(gifProvider, times(1)).search(any(), any(), any(), onSuccessCaptor.capture(), any()) + val capturedProviderOnSuccess = onSuccessCaptor.firstValue + capturedProviderOnSuccess(expectedGifMediaViewModelCollection, 2) + + verify(spiedDataSource, times(1)).retryAllFailedRangeLoads() + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/giphy/GiphyPickerViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/gif/GifPickerViewModelTest.kt similarity index 87% rename from WordPress/src/test/java/org/wordpress/android/viewmodel/giphy/GiphyPickerViewModelTest.kt rename to WordPress/src/test/java/org/wordpress/android/viewmodel/gif/GifPickerViewModelTest.kt index 4c3480cdfd52..01179c5b952a 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/giphy/GiphyPickerViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/gif/GifPickerViewModelTest.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.viewmodel.giphy +package org.wordpress.android.viewmodel.gif import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.paging.PositionalDataSource.LoadInitialCallback @@ -19,29 +19,32 @@ import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner import org.wordpress.android.fluxc.model.MediaModel import org.wordpress.android.util.NetworkUtilsWrapper -import org.wordpress.android.viewmodel.giphy.GiphyPickerViewModel.EmptyDisplayMode -import org.wordpress.android.viewmodel.giphy.GiphyPickerViewModel.State +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import org.wordpress.android.viewmodel.gif.GifPickerViewModel.EmptyDisplayMode +import org.wordpress.android.viewmodel.gif.GifPickerViewModel.State import java.util.Random import java.util.UUID @RunWith(MockitoJUnitRunner::class) -class GiphyPickerViewModelTest { +class GifPickerViewModelTest { @get:Rule val rule = InstantTaskExecutorRule() - private lateinit var viewModel: GiphyPickerViewModel + private lateinit var viewModel: GifPickerViewModel - private val dataSourceFactory = mock() - private val mediaFetcher = mock() + private val dataSourceFactory = mock() + private val mediaFetcher = mock() + private val analyticsTracker = mock() @Mock private lateinit var networkUtils: NetworkUtilsWrapper @Before fun setUp() { - viewModel = GiphyPickerViewModel( + viewModel = GifPickerViewModel( dataSourceFactory = dataSourceFactory, networkUtils = networkUtils, - mediaFetcher = mediaFetcher + mediaFetcher = mediaFetcher, + analyticsTrackerWrapper = analyticsTracker ) viewModel.setup(site = mock()) whenever(networkUtils.isNetworkAvailable()).thenReturn(true) @@ -49,7 +52,7 @@ class GiphyPickerViewModelTest { @Test fun `when setting a mediaViewModel as selected, it adds that to the selected list`() { - val mediaViewModel = createGiphyMediaViewModel() + val mediaViewModel = createGifMediaViewModel() viewModel.toggleSelected(mediaViewModel) @@ -61,7 +64,7 @@ class GiphyPickerViewModelTest { @Test fun `when setting a mediaViewModel as selected, it updates the isSelected and selectedNumber`() { - val mediaViewModel = createGiphyMediaViewModel() + val mediaViewModel = createGifMediaViewModel() viewModel.toggleSelected(mediaViewModel) @@ -72,7 +75,7 @@ class GiphyPickerViewModelTest { @Test fun `when toggling an already selected mediaViewModel, it gets deselected and removed from the selected list`() { // Arrange - val mediaViewModel = createGiphyMediaViewModel() + val mediaViewModel = createGifMediaViewModel() viewModel.toggleSelected(mediaViewModel) // Act @@ -88,10 +91,10 @@ class GiphyPickerViewModelTest { @Test fun `when deselecting a mediaViewModel, it rebuilds the selectedNumbers so they are continuous`() { // Arrange - val alpha = createGiphyMediaViewModel() - val bravo = createGiphyMediaViewModel() - val charlie = createGiphyMediaViewModel() - val delta = createGiphyMediaViewModel() + val alpha = createGifMediaViewModel() + val bravo = createGifMediaViewModel() + val charlie = createGifMediaViewModel() + val delta = createGifMediaViewModel() listOf(alpha, bravo, charlie, delta).forEach(viewModel::toggleSelected) @@ -118,7 +121,7 @@ class GiphyPickerViewModelTest { @Test fun `when the searchQuery is changed, it clears the selected mediaViewModel list`() { // Arrange - val mediaViewModel = createGiphyMediaViewModel() + val mediaViewModel = createGifMediaViewModel() viewModel.toggleSelected(mediaViewModel) // Act @@ -147,12 +150,12 @@ class GiphyPickerViewModelTest { @Test fun `when search results are empty, the empty view should be visible and says there are no results`() { // Arrange - val dataSource = mock() + val dataSource = mock() whenever(dataSourceFactory.create()).thenReturn(dataSource) whenever(dataSourceFactory.searchQuery).thenReturn("dummy") - val callbackCaptor = argumentCaptor>() + val callbackCaptor = argumentCaptor>() doNothing().whenever(dataSource).loadInitial(any(), callbackCaptor.capture()) // Observe mediaViewModelPagedList so the DataSourceFactory will be activated and perform API requests @@ -174,12 +177,12 @@ class GiphyPickerViewModelTest { @Test fun `when the initial load fails, the empty view should show a network error`() { // Arrange - val dataSource = mock() + val dataSource = mock() whenever(dataSourceFactory.create()).thenReturn(dataSource) whenever(dataSourceFactory.initialLoadError).thenReturn(mock()) - val callbackCaptor = argumentCaptor>() + val callbackCaptor = argumentCaptor>() doNothing().whenever(dataSource).loadInitial(any(), callbackCaptor.capture()) // Observe mediaViewModelPagedList so the DataSourceFactory will be activated and perform API requests @@ -284,7 +287,7 @@ class GiphyPickerViewModelTest { } check(viewModel.state.value == State.FINISHED) - viewModel.toggleSelected(createGiphyMediaViewModel()) + viewModel.toggleSelected(createGifMediaViewModel()) // Assert assertThat(viewModel.selectedMediaViewModelList.value).isNull() @@ -313,7 +316,7 @@ class GiphyPickerViewModelTest { id = Random().nextInt() } - private fun createGiphyMediaViewModel() = MutableGiphyMediaViewModel( + private fun createGifMediaViewModel() = MutableGifMediaViewModel( id = UUID.randomUUID().toString(), thumbnailUri = mock(), largeImageUri = mock(), diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/gif/provider/TenorProviderTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/gif/provider/TenorProviderTest.kt new file mode 100644 index 000000000000..109a17270817 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/gif/provider/TenorProviderTest.kt @@ -0,0 +1,247 @@ +package org.wordpress.android.viewmodel.gif.provider + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import com.tenor.android.core.constant.MediaFilter +import com.tenor.android.core.network.ApiClient +import com.tenor.android.core.network.ApiService.Builder +import com.tenor.android.core.network.IApiClient +import com.tenor.android.core.response.impl.GifsResponse +import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.wordpress.android.BuildConfig +import org.wordpress.android.TestApplication +import org.wordpress.android.viewmodel.gif.provider.GifProvider.GifRequestFailedException +import org.wordpress.android.viewmodel.gif.provider.TenorProviderTestFixtures.expectedGifMediaViewModelCollection +import org.wordpress.android.viewmodel.gif.provider.TenorProviderTestFixtures.mockedTenorResult +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +@Config(application = TestApplication::class) +@RunWith(RobolectricTestRunner::class) +class TenorProviderTest { + @Mock lateinit var apiClient: IApiClient + + @Mock lateinit var gifSearchCall: Call + + @Mock lateinit var callbackResponse: Response + + @Mock lateinit var gifResponse: GifsResponse + + @Captor lateinit var callbackCaptor: ArgumentCaptor> + + private lateinit var tenorProviderUnderTest: TenorProvider + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + val context = ApplicationProvider.getApplicationContext() + + Builder(context, IApiClient::class.java).apply { + apiKey(BuildConfig.TENOR_API_KEY) + ApiClient.init(context, this) + } + + whenever(apiClient.search(any(), any(), any(), any(), any(), any())) + .thenReturn(gifSearchCall) + + val gifResults = mockedTenorResult + whenever(gifResponse.results).thenReturn(gifResults) + whenever(gifResponse.next).thenReturn("0") + whenever(callbackResponse.body()).thenReturn(gifResponse) + + tenorProviderUnderTest = TenorProvider(context, apiClient) + } + + @Test + fun `search call should invoke onSuccess with expected GIF list and nextPosition for valid query`() { + var onSuccessWasCalled = false + + tenorProviderUnderTest.search("test", + 0, + onSuccess = { actualViewModelCollection, _ -> + onSuccessWasCalled = true + assertThat(actualViewModelCollection).isEqualTo(expectedGifMediaViewModelCollection) + }, + onFailure = { + fail("Failure handler should not be called") + }) + + verify(gifSearchCall, times(1)).enqueue(callbackCaptor.capture()) + val capturedCallback = callbackCaptor.value + capturedCallback.onResponse(gifSearchCall, callbackResponse) + assertThat(onSuccessWasCalled).isTrue() + } + + @Test + fun `search call should invoke onSuccess with expected nextPosition for a valid query`() { + var onSuccessWasCalled = false + val expectedNextPosition = 0 + + tenorProviderUnderTest.search("test", + 0, + onSuccess = { _, actualNextPosition -> + onSuccessWasCalled = true + assertThat(actualNextPosition).isEqualTo(expectedNextPosition) + }, + onFailure = { + fail("Failure handler should not be called") + }) + + verify(gifSearchCall, times(1)).enqueue(callbackCaptor.capture()) + val capturedCallback = callbackCaptor.value + capturedCallback.onResponse(gifSearchCall, callbackResponse) + assertThat(onSuccessWasCalled).isTrue() + } + + @Test + fun `search call should invoke onFailure when callback returns failure`() { + var onFailureWasCalled = false + + tenorProviderUnderTest.search("test", + 0, + onSuccess = { _, _ -> + fail("Success handler should not be called") + }, + onFailure = { throwable -> + onFailureWasCalled = true + assertThat(throwable).isInstanceOf(GifRequestFailedException::class.java) + assertThat(throwable.message).isEqualTo("Expected message") + }) + + verify(gifSearchCall, times(1)).enqueue(callbackCaptor.capture()) + val capturedCallback = callbackCaptor.value + capturedCallback.onFailure(gifSearchCall, RuntimeException("Expected message")) + assertThat(onFailureWasCalled).isTrue() + } + + @Test + fun `search call should invoke onFailure when null GifResponse is returned`() { + var onFailureWasCalled = false + whenever(callbackResponse.body()).thenReturn(null) + + tenorProviderUnderTest.search("test", + 0, + onSuccess = { _, _ -> + fail("Success handler should not be called") + }, + onFailure = { throwable -> + onFailureWasCalled = true + assertThat(throwable).isInstanceOf(GifRequestFailedException::class.java) + assertThat(throwable.message).isEqualTo("No media matching your search") + }) + + verify(gifSearchCall, times(1)).enqueue(callbackCaptor.capture()) + val capturedCallback = callbackCaptor.value + capturedCallback.onResponse(gifSearchCall, callbackResponse) + assertThat(onFailureWasCalled).isTrue() + } + + @Test + fun `search call must use BASIC as MediaFilter`() { + val argument = ArgumentCaptor.forClass(String::class.java) + + tenorProviderUnderTest.search( + "test", + 0, + onSuccess = { _, _ -> }, + onFailure = {}) + + verify(apiClient).search( + any(), + any(), + any(), + any(), + argument.capture(), + any() + ) + + val requestedMediaFilter = argument.value + assertThat(requestedMediaFilter).isEqualTo(MediaFilter.BASIC) + } + + @Test + fun `search call without loadSize should use default maximum value`() { + val argument = ArgumentCaptor.forClass(Int::class.java) + + tenorProviderUnderTest.search( + "test", + 0, + onSuccess = { _, _ -> }, + onFailure = {}) + + verify(apiClient).search( + any(), + any(), + argument.capture(), + any(), + any(), + any() + ) + + val requestedLoadSize = argument.value + assertThat(requestedLoadSize).isEqualTo(50) + } + + @Test + fun `search call with loadSize lower than 50 should be used`() { + val argument = ArgumentCaptor.forClass(Int::class.java) + + tenorProviderUnderTest.search( + "test", + 0, + 20, + onSuccess = { _, _ -> }, + onFailure = {}) + + verify(apiClient).search( + any(), + any(), + argument.capture(), + any(), + any(), + any() + ) + + val requestedLoadSize = argument.value + assertThat(requestedLoadSize).isEqualTo(20) + } + + @Test + fun `search call with loadSize higher than 50 should be reduced back to default maximum value`() { + val argument = ArgumentCaptor.forClass(Int::class.java) + + tenorProviderUnderTest.search( + "test", + 0, + 1500, + onSuccess = { _, _ -> }, + onFailure = {}) + + verify(apiClient).search( + any(), + any(), + argument.capture(), + any(), + any(), + any() + ) + + val requestedLoadSize = argument.value + assertThat(requestedLoadSize).isEqualTo(50) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/gif/provider/TenorProviderTestFixtures.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/gif/provider/TenorProviderTestFixtures.kt new file mode 100644 index 000000000000..7a3ef1cd6ce5 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/gif/provider/TenorProviderTestFixtures.kt @@ -0,0 +1,58 @@ +package org.wordpress.android.viewmodel.gif.provider + +import android.net.Uri +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever +import com.tenor.android.core.constant.MediaCollectionFormat +import com.tenor.android.core.model.impl.Media +import com.tenor.android.core.model.impl.MediaCollection +import com.tenor.android.core.model.impl.Result +import org.wordpress.android.viewmodel.gif.MutableGifMediaViewModel + +object TenorProviderTestFixtures { + internal val mockedTenorResult + get() = listOf( + createGifResultMock("first GIF"), + createGifResultMock("second GIF"), + createGifResultMock("third GIF"), + createGifResultMock("fourth GIF") + ) + + internal val expectedGifMediaViewModelCollection = listOf( + createExpectedGifMediaViewModel("first GIF"), + createExpectedGifMediaViewModel("second GIF"), + createExpectedGifMediaViewModel("third GIF"), + createExpectedGifMediaViewModel("fourth GIF") + ) + + private fun createGifResultMock(mockFilling: String) = + mock().apply { + whenever(this.id).thenReturn(mockFilling) + whenever(this.title).thenReturn(mockFilling) + val mediaCollection = createMediaCollectionMock(mockFilling) + whenever(this.medias).thenReturn(listOf(mediaCollection)) + } + + private fun createMediaCollectionMock(mockContent: String) = + mock().apply { + val nanoGifMedia = createMediaMock("$mockContent gif_nano") + val tinyGifMedia = createMediaMock("$mockContent gif_tiny") + val gifMedia = createMediaMock("$mockContent gif") + + whenever(this[MediaCollectionFormat.GIF_NANO]).thenReturn(nanoGifMedia) + whenever(this[MediaCollectionFormat.GIF_TINY]).thenReturn(tinyGifMedia) + whenever(this[MediaCollectionFormat.GIF]).thenReturn(gifMedia) + } + + private fun createMediaMock(mockContent: String) = + mock().apply { whenever(this.url).thenReturn(mockContent) } + + private fun createExpectedGifMediaViewModel(expectedContent: String) = + MutableGifMediaViewModel( + expectedContent, + Uri.parse("$expectedContent gif_nano"), + Uri.parse("$expectedContent gif_tiny"), + Uri.parse("$expectedContent gif"), + expectedContent + ) +} diff --git a/gradle.properties-example b/gradle.properties-example index a650834a517b..0855ee223728 100644 --- a/gradle.properties-example +++ b/gradle.properties-example @@ -26,6 +26,7 @@ wp.zendesk.domain=https://www.google.com/ wp.zendesk.oauth_client_id=wordpress wp.reset_db_on_downgrade = false wp.sentry.dsn=https://00000000000000000000000000000000@sentry.io/00000000 +wp.tenor.api_key=wordpress # Needed to use the Google Maps component aka the PlacePicker (Post Settings -> Location) wp.res.com.google.android.geo.api.key = geo-api-key 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 4bad16dca558..7415d78f5316 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 @@ -483,9 +483,9 @@ public enum Stat { STOCK_MEDIA_ACCESSED, STOCK_MEDIA_SEARCHED, STOCK_MEDIA_UPLOADED, - GIPHY_PICKER_SEARCHED, - GIPHY_PICKER_ACCESSED, - GIPHY_PICKER_DOWNLOADED, + GIF_PICKER_SEARCHED, + GIF_PICKER_ACCESSED, + GIF_PICKER_DOWNLOADED, SHORTCUT_STATS_CLICKED, SHORTCUT_NOTIFICATIONS_CLICKED, SHORTCUT_NEW_POST_CLICKED, 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 ea2d36fcee0a..60a58e7fbc51 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 @@ -1502,12 +1502,12 @@ public static String getEventNameForStat(AnalyticsTracker.Stat stat) { return "stock_media_searched"; case STOCK_MEDIA_UPLOADED: return "stock_media_uploaded"; - case GIPHY_PICKER_SEARCHED: - return "giphy_picker_searched"; - case GIPHY_PICKER_ACCESSED: - return "giphy_picker_accessed"; - case GIPHY_PICKER_DOWNLOADED: - return "giphy_picker_downloaded"; + case GIF_PICKER_SEARCHED: + return "gif_picker_searched"; + case GIF_PICKER_ACCESSED: + return "gif_picker_accessed"; + case GIF_PICKER_DOWNLOADED: + return "gif_picker_downloaded"; case SHORTCUT_STATS_CLICKED: return "shortcut_stats_clicked"; case SHORTCUT_NOTIFICATIONS_CLICKED: