From 521bd2cce4d7ccb81780c20778699a39852601e3 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 24 Jun 2022 15:34:17 -0300 Subject: [PATCH] Implement first-time-nav screen for stories. --- .../securesms/keyvalue/StoryValues.kt | 15 +- .../stories/StoryFirstTimeNavigationView.kt | 120 + .../viewer/page/StoryFirstNavigationStub.kt | 44 + .../viewer/page/StoryViewerPageFragment.kt | 46 +- .../viewer/page/StoryViewerPageViewModel.kt | 4 + .../viewer/page/StoryViewerPlaybackState.kt | 6 +- .../main/res/drawable/ic_swipe_right_72.xml | 25 + app/src/main/res/drawable/ic_swipe_up_72.xml | 34 + app/src/main/res/drawable/ic_tap_72.xml | 13 + .../layout/stories_viewer_fragment_page.xml | 13 +- .../story_first_time_navigation_view.xml | 111 + ...tory_viewer_first_time_navigation_stub.xml | 7 + app/src/main/res/values/strings.xml | 9 + .../StoryFirstTimeNavigationViewTest.kt | 159 ++ dependencies.gradle | 9 +- gradle/verification-metadata.xml | 2328 ++++++++++++++++- 16 files changed, 2840 insertions(+), 103 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/stories/StoryFirstTimeNavigationView.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryFirstNavigationStub.kt create mode 100644 app/src/main/res/drawable/ic_swipe_right_72.xml create mode 100644 app/src/main/res/drawable/ic_swipe_up_72.xml create mode 100644 app/src/main/res/drawable/ic_tap_72.xml create mode 100644 app/src/main/res/layout/story_first_time_navigation_view.xml create mode 100644 app/src/main/res/layout/story_viewer_first_time_navigation_stub.xml create mode 100644 app/src/test/java/org/thoughtcrime/securesms/stories/StoryFirstTimeNavigationViewTest.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StoryValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StoryValues.kt index 205aa071a..bb7a15790 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StoryValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StoryValues.kt @@ -34,11 +34,22 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) { * Cannot send to story tooltip marker */ private const val CANNOT_SEND_SEEN_MARKER = "stories.cannot.send.video.tooltip.seen" + + /** + * Whether or not the user has see the "Navigation education" view + */ + private const val USER_HAS_SEEN_FIRST_NAV_VIEW = "stories.user.has.seen.first.navigation.view" } override fun onFirstEverAppLaunch() = Unit - override fun getKeysToIncludeInBackup(): MutableList = mutableListOf(MANUAL_FEATURE_DISABLE, USER_HAS_ADDED_TO_A_STORY, VIDEO_TOOLTIP_SEEN_MARKER, CANNOT_SEND_SEEN_MARKER) + override fun getKeysToIncludeInBackup(): MutableList = mutableListOf( + MANUAL_FEATURE_DISABLE, + USER_HAS_ADDED_TO_A_STORY, + VIDEO_TOOLTIP_SEEN_MARKER, + CANNOT_SEND_SEEN_MARKER, + USER_HAS_SEEN_FIRST_NAV_VIEW + ) var isFeatureDisabled: Boolean by booleanValue(MANUAL_FEATURE_DISABLE, false) @@ -50,6 +61,8 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) { var cannotSendTooltipSeen by booleanValue(CANNOT_SEND_SEEN_MARKER, false) + var userHasSeenFirstNavView: Boolean by booleanValue(USER_HAS_SEEN_FIRST_NAV_VIEW, false) + fun setLatestStorySend(storySend: StorySend) { synchronized(this) { val storySends: List = getList(LATEST_STORY_SENDS, StorySendSerializer) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryFirstTimeNavigationView.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryFirstTimeNavigationView.kt new file mode 100644 index 000000000..b7ad4e490 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryFirstTimeNavigationView.kt @@ -0,0 +1,120 @@ +package org.thoughtcrime.securesms.stories + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import androidx.constraintlayout.widget.ConstraintLayout +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import org.signal.core.util.DimensionUnit +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.blurhash.BlurHash +import org.thoughtcrime.securesms.components.CornerMask +import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.util.visible + +class StoryFirstTimeNavigationView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : ConstraintLayout(context, attrs) { + + companion object { + private const val BLUR_ALPHA = 0x3D + private const val NO_BLUR_ALPHA = 0xB3 + } + + init { + inflate(context, R.layout.story_first_time_navigation_view, this) + } + + private val blurHashView: ImageView = findViewById(R.id.edu_blur_hash) + private val overlayView: ImageView = findViewById(R.id.edu_overlay) + private val gotIt: View = findViewById(R.id.edu_got_it) + + private val cornerMask = CornerMask(this).apply { + setRadius(DimensionUnit.DP.toPixels(18f).toInt()) + } + + var callback: Callback? = null + + init { + if (isRenderEffectSupported()) { + blurHashView.visible = false + overlayView.visible = true + overlayView.setImageDrawable(ColorDrawable(Color.argb(BLUR_ALPHA, 0, 0, 0))) + } + + gotIt.setOnClickListener { + callback?.onGotItClicked() + GlideApp.with(this).clear(blurHashView) + blurHashView.setImageDrawable(null) + hide() + } + } + + override fun dispatchDraw(canvas: Canvas) { + super.dispatchDraw(canvas) + cornerMask.mask(canvas) + } + + fun setBlurHash(blurHash: BlurHash?) { + if (isRenderEffectSupported() || callback?.userHasSeenFirstNavigationView() == true) { + return + } + + if (blurHash == null) { + blurHashView.visible = false + overlayView.visible = true + overlayView.setImageDrawable(ColorDrawable(Color.argb(NO_BLUR_ALPHA, 0, 0, 0))) + GlideApp.with(this).clear(blurHashView) + return + } else { + blurHashView.visible = true + overlayView.visible = true + overlayView.setImageDrawable(ColorDrawable(Color.argb(BLUR_ALPHA, 0, 0, 0))) + } + + GlideApp.with(this) + .load(blurHash) + .addListener(object : RequestListener { + override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { + setBlurHash(null) + return false + } + + override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { + return false + } + }) + .into(blurHashView) + } + + fun show() { + if (callback?.userHasSeenFirstNavigationView() == true) { + return + } + + visible = true + } + + fun hide() { + visible = false + } + + private fun isRenderEffectSupported(): Boolean { + return Build.VERSION.SDK_INT >= 31 + } + + interface Callback { + fun userHasSeenFirstNavigationView(): Boolean + fun onGotItClicked() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryFirstNavigationStub.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryFirstNavigationStub.kt new file mode 100644 index 000000000..21f122bec --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryFirstNavigationStub.kt @@ -0,0 +1,44 @@ +package org.thoughtcrime.securesms.stories.viewer.page + +import android.view.ViewStub +import org.thoughtcrime.securesms.blurhash.BlurHash +import org.thoughtcrime.securesms.stories.StoryFirstTimeNavigationView +import org.thoughtcrime.securesms.util.views.Stub + +/** + * Specialized stub that allows for early arrival of the blurhash and callback. + */ +class StoryFirstNavigationStub(viewStub: ViewStub) : Stub(viewStub) { + + private var callback: StoryFirstTimeNavigationView.Callback? = null + private var blurHash: BlurHash? = null + + fun setCallback(callback: StoryFirstTimeNavigationView.Callback) { + if (resolved()) { + get().callback = callback + } else { + this.callback = callback + } + } + + fun setBlurHash(blurHash: BlurHash?) { + if (resolved()) { + get().setBlurHash(blurHash) + } else { + this.blurHash = blurHash + } + } + + override fun get(): StoryFirstTimeNavigationView { + val needsResolve = !resolved() + val view = super.get() + if (needsResolve) { + view.setBlurHash(blurHash) + view.callback = callback + blurHash = null + callback = null + } + + return view + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt index 59f370087..22fcb459f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt @@ -4,8 +4,11 @@ import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.Context +import android.graphics.RenderEffect +import android.graphics.Shader import android.graphics.drawable.Drawable import android.net.Uri +import android.os.Build import android.os.Bundle import android.view.GestureDetector import android.view.MotionEvent @@ -20,6 +23,7 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.core.view.GestureDetectorCompat import androidx.core.view.animation.PathInterpolatorCompat import androidx.core.view.doOnNextLayout +import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -42,11 +46,13 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment import org.thoughtcrime.securesms.mediapreview.VideoControlsDelegate import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.stories.StoryFirstTimeNavigationView import org.thoughtcrime.securesms.stories.StorySlateView import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu import org.thoughtcrime.securesms.stories.viewer.StoryViewerState @@ -78,7 +84,8 @@ class StoryViewerPageFragment : MultiselectForwardBottomSheet.Callback, StorySlateView.Callback, StoryTextPostPreviewFragment.Callback, - StoriesSharedElementCrossFaderView.Callback { + StoriesSharedElementCrossFaderView.Callback, + StoryFirstTimeNavigationView.Callback { private lateinit var progressBar: SegmentedProgressBar private lateinit var storySlate: StorySlateView @@ -87,6 +94,7 @@ class StoryViewerPageFragment : private lateinit var blurContainer: ImageView private lateinit var storyCaptionContainer: FrameLayout private lateinit var storyContentContainer: FrameLayout + private lateinit var storyFirstTimeNavigationViewStub: StoryFirstNavigationStub private lateinit var callback: Callback @@ -150,9 +158,11 @@ class StoryViewerPageFragment : progressBar = view.findViewById(R.id.progress) viewsAndReplies = view.findViewById(R.id.views_and_replies_bar) storyCrossfader = view.findViewById(R.id.story_content_crossfader) + storyFirstTimeNavigationViewStub = StoryFirstNavigationStub(view.findViewById(R.id.story_first_time_nav_stub)) storySlate.callback = this storyCrossfader.callback = this + storyFirstTimeNavigationViewStub.setCallback(this) chrome = listOf( closeView, @@ -165,7 +175,7 @@ class StoryViewerPageFragment : viewsAndReplies, progressBar, storyGradientTop, - storyGradientBottom + storyGradientBottom, ) senderAvatar.setFallbackPhotoProvider(FallbackPhotoProvider()) @@ -338,20 +348,37 @@ class StoryViewerPageFragment : resumeProgress() } + val wasDisplayingNavigationView = isFirstTimeNavVisible() + when { state.hideChromeImmediate -> { hideChromeImmediate() storyCaptionContainer.visible = false + storyFirstTimeNavigationViewStub.takeIf { it.resolved() }?.get()?.hide() } state.hideChrome -> { hideChrome() storyCaptionContainer.visible = true + storyFirstTimeNavigationViewStub.get().show() } else -> { showChrome() storyCaptionContainer.visible = true + storyFirstTimeNavigationViewStub.get().show() } } + + val isDisplayingNavigationView = isFirstTimeNavVisible() + if (isDisplayingNavigationView && Build.VERSION.SDK_INT >= 31) { + hideChromeImmediate() + storyContentContainer.setRenderEffect(RenderEffect.createBlurEffect(100f, 100f, Shader.TileMode.CLAMP)) + } else if (Build.VERSION.SDK_INT >= 31) { + storyContentContainer.setRenderEffect(null) + } + + if (wasDisplayingNavigationView xor isDisplayingNavigationView) { + viewModel.setIsDisplayingFirstTimeNavigation(isFirstTimeNavVisible()) + } } timeoutDisposable.bindTo(viewLifecycleOwner) @@ -402,6 +429,10 @@ class StoryViewerPageFragment : viewModel.setIsDisplayingForwardDialog(false) } + private fun isFirstTimeNavVisible(): Boolean { + return storyFirstTimeNavigationViewStub.takeIf { it.resolved() }?.get()?.isVisible ?: false + } + private fun calculateDurationForText(textContent: StoryPost.Content.TextContent): Long { val divisionsOf15 = textContent.length / CHARACTERS_PER_SECOND return TimeUnit.SECONDS.toMillis(divisionsOf15) + MIN_TEXT_STORY_PLAYBACK @@ -623,6 +654,8 @@ class StoryViewerPageFragment : val record = storyPost.conversationMessage.messageRecord as? MediaMmsMessageRecord val blurHash = record?.slideDeck?.thumbnailSlide?.placeholderBlur + storyFirstTimeNavigationViewStub.setBlurHash(blurHash) + if (blurHash == null) { GlideApp.with(blur).clear(blur) } else { @@ -1035,4 +1068,13 @@ class StoryViewerPageFragment : fun onFinishedPosts(recipientId: RecipientId) fun onStoryHidden(recipientId: RecipientId) } + + override fun userHasSeenFirstNavigationView(): Boolean { + return SignalStore.storyValues().userHasSeenFirstNavView + } + + override fun onGotItClicked() { + SignalStore.storyValues().userHasSeenFirstNavView = true + viewModel.setIsDisplayingFirstTimeNavigation(false) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt index 2752de89f..88ad50bed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt @@ -214,6 +214,10 @@ class StoryViewerPageViewModel( storyViewerPlaybackStore.update { it.copy(isRunningSharedElementAnimation = isRunningSharedElementAnimation) } } + fun setIsDisplayingFirstTimeNavigation(isDisplayingFirstTimeNavigation: Boolean) { + storyViewerPlaybackStore.update { it.copy(isDisplayingFirstTimeNavigation = isDisplayingFirstTimeNavigation) } + } + private fun resolveSwipeToReplyState(state: StoryViewerPageState, index: Int): StoryViewerPageState.ReplyState { if (index !in state.posts.indices) { return StoryViewerPageState.ReplyState.NONE diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPlaybackState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPlaybackState.kt index ad9483562..1c6c47e39 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPlaybackState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPlaybackState.kt @@ -15,7 +15,8 @@ data class StoryViewerPlaybackState( val isFragmentResumed: Boolean = false, val isDisplayingLinkPreviewTooltip: Boolean = false, val isDisplayingReactionAnimation: Boolean = false, - val isRunningSharedElementAnimation: Boolean = false + val isRunningSharedElementAnimation: Boolean = false, + val isDisplayingFirstTimeNavigation: Boolean = false ) { val hideChromeImmediate: Boolean = isRunningSharedElementAnimation @@ -36,5 +37,6 @@ data class StoryViewerPlaybackState( !isFragmentResumed || isDisplayingLinkPreviewTooltip || isDisplayingReactionAnimation || - isRunningSharedElementAnimation + isRunningSharedElementAnimation || + isDisplayingFirstTimeNavigation } diff --git a/app/src/main/res/drawable/ic_swipe_right_72.xml b/app/src/main/res/drawable/ic_swipe_right_72.xml new file mode 100644 index 000000000..72a1321a9 --- /dev/null +++ b/app/src/main/res/drawable/ic_swipe_right_72.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_swipe_up_72.xml b/app/src/main/res/drawable/ic_swipe_up_72.xml new file mode 100644 index 000000000..54a92169d --- /dev/null +++ b/app/src/main/res/drawable/ic_swipe_up_72.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_tap_72.xml b/app/src/main/res/drawable/ic_tap_72.xml new file mode 100644 index 000000000..189348d90 --- /dev/null +++ b/app/src/main/res/drawable/ic_tap_72.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/layout/stories_viewer_fragment_page.xml b/app/src/main/res/layout/stories_viewer_fragment_page.xml index 7e102e00b..6b1fd03af 100644 --- a/app/src/main/res/layout/stories_viewer_fragment_page.xml +++ b/app/src/main/res/layout/stories_viewer_fragment_page.xml @@ -122,8 +122,8 @@ android:layout_marginEnd="16dp" android:layout_marginBottom="24dp" android:gravity="bottom" - android:textColor="@color/signal_colorNeutralInverse" android:textAppearance="@style/Signal.Text.BodyLarge" + android:textColor="@color/signal_colorNeutralInverse" tools:text="Ugh." /> @@ -217,8 +217,8 @@ android:layout_marginStart="8dp" android:layout_marginEnd="12dp" android:alpha="0.64" - android:textColor="@color/signal_colorNeutralVariantInverse" android:textAppearance="@style/Signal.Text.MaterialCaption" + android:textColor="@color/signal_colorNeutralVariantInverse" app:layout_constraintBottom_toBottomOf="@id/from" app:layout_constraintEnd_toStartOf="@id/more" app:layout_constraintStart_toEndOf="@id/from" @@ -278,4 +278,13 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/story_first_time_navigation_view.xml b/app/src/main/res/layout/story_first_time_navigation_view.xml new file mode 100644 index 000000000..45883e89b --- /dev/null +++ b/app/src/main/res/layout/story_first_time_navigation_view.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/story_viewer_first_time_navigation_stub.xml b/app/src/main/res/layout/story_viewer_first_time_navigation_stub.xml new file mode 100644 index 000000000..dd6a097e5 --- /dev/null +++ b/app/src/main/res/layout/story_viewer_first_time_navigation_stub.xml @@ -0,0 +1,7 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 57f6aaa71..cd11af6ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4903,6 +4903,15 @@ Expired + + Tap to advance + + Swipe up to skip + + Swipe right to exit + + Got it + diff --git a/app/src/test/java/org/thoughtcrime/securesms/stories/StoryFirstTimeNavigationViewTest.kt b/app/src/test/java/org/thoughtcrime/securesms/stories/StoryFirstTimeNavigationViewTest.kt new file mode 100644 index 000000000..e5bb23ba0 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/stories/StoryFirstTimeNavigationViewTest.kt @@ -0,0 +1,159 @@ +package org.thoughtcrime.securesms.stories + +import android.app.Application +import android.graphics.drawable.Drawable +import android.os.Looper.getMainLooper +import android.view.ContextThemeWrapper +import android.view.View +import android.widget.ImageView +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockedStatic +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config +import org.thoughtcrime.securesms.blurhash.BlurHash +import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.mms.GlideRequest +import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.util.visible + +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class StoryFirstTimeNavigationViewTest { + + @Rule + @JvmField + val mockitoRule: MockitoRule = MockitoJUnit.rule() + + private lateinit var testSubject: StoryFirstTimeNavigationView + + @Mock + private lateinit var glideApp: MockedStatic + + @Mock + private lateinit var glideRequests: GlideRequests + + @Mock + private lateinit var glideRequest: GlideRequest + + @Before + fun setUp() { + testSubject = StoryFirstTimeNavigationView(ContextThemeWrapper(ApplicationProvider.getApplicationContext(), org.thoughtcrime.securesms.R.style.Signal_DayNight)) + + whenever(GlideApp.with(any())).thenReturn(glideRequests) + whenever(glideRequests.load(any())).thenReturn(glideRequest) + whenever(glideRequest.addListener(any())).thenReturn(glideRequest) + } + + @Test + @Config(sdk = [31]) + fun `Given sdk 31, when I create testSubject, then I expect overlay visible and blur hash not visible`() { + shadowOf(getMainLooper()).idle() + + assertTrue(testSubject.findViewById(org.thoughtcrime.securesms.R.id.edu_overlay).visible) + assertFalse(testSubject.findViewById(org.thoughtcrime.securesms.R.id.edu_blur_hash).visible) + } + + @Test + @Config(sdk = [30]) + fun `Given sdk 30, when I create testSubject, then I expect overlay visible and blur hash visible`() { + shadowOf(getMainLooper()).idle() + + assertTrue(testSubject.findViewById(org.thoughtcrime.securesms.R.id.edu_overlay).visible) + assertTrue(testSubject.findViewById(org.thoughtcrime.securesms.R.id.edu_blur_hash).visible) + } + + @Test + @Config(sdk = [31]) + fun `Given sdk 31 when I set blur hash, then blur has is visible`() { + shadowOf(getMainLooper()).idle() + + testSubject.setBlurHash(BlurHash.parseOrNull("0000")!!) + + assertFalse(testSubject.findViewById(org.thoughtcrime.securesms.R.id.edu_blur_hash).visible) + } + + @Test + @Config(sdk = [30]) + fun `Given sdk 30, when I set blur hash, then blur hash is loaded`() { + shadowOf(getMainLooper()).idle() + + testSubject.setBlurHash(BlurHash.parseOrNull("0000")!!) + + val blurHashView = testSubject.findViewById(org.thoughtcrime.securesms.R.id.edu_blur_hash) + verify(glideRequest).into(eq(blurHashView)) + } + + @Test + @Config(sdk = [30]) + fun `Given sdk 30, when I set blur hash to null, then blur hash is hidden and cleared`() { + shadowOf(getMainLooper()).idle() + + testSubject.setBlurHash(null) + + val blurHashView = testSubject.findViewById(org.thoughtcrime.securesms.R.id.edu_blur_hash) + assertFalse(blurHashView.visible) + verify(glideRequests).clear(blurHashView) + } + + @Test + @Config(sdk = [30]) + fun `Given sdk 30 and user has seen overlay, when I set blur hash, then nothing happens`() { + shadowOf(getMainLooper()).idle() + testSubject.callback = object : StoryFirstTimeNavigationView.Callback { + override fun userHasSeenFirstNavigationView(): Boolean = true + override fun onGotItClicked() = error("Unused") + } + + testSubject.setBlurHash(BlurHash.parseOrNull("0000")!!) + + val blurHashView = testSubject.findViewById(org.thoughtcrime.securesms.R.id.edu_blur_hash) + verify(glideRequest, never()).into(eq(blurHashView)) + } + + @Test + @Config + fun `When I hide then I expect not to be visible`() { + testSubject.hide() + + assertFalse(testSubject.visible) + } + + @Test + @Config + fun `Given I hide, when I show, then I expect to be visible`() { + testSubject.hide() + + testSubject.show() + + assertTrue(testSubject.visible) + } + + @Test + @Config + fun `Given I hide and user has seen overlay, when I show, then I expect to not be visible`() { + testSubject.hide() + testSubject.callback = object : StoryFirstTimeNavigationView.Callback { + override fun userHasSeenFirstNavigationView(): Boolean = true + override fun onGotItClicked() = error("Unused") + } + + testSubject.show() + + assertFalse(testSubject.visible) + } +} diff --git a/dependencies.gradle b/dependencies.gradle index b742d7396..54a03bcdb 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -123,14 +123,15 @@ dependencyResolutionManagement { } testLibs { - version('robolectric', '4.4') + version('robolectric', '4.8.1') + version('androidx-test', '1.4.0') alias('junit-junit').to('junit:junit:4.13.2') - alias('androidx-test-core').to('androidx.test:core:1.2.0') - alias('androidx-test-core-ktx').to('androidx.test:core-ktx:1.2.0') + alias('androidx-test-core').to('androidx.test', 'core').versionRef('androidx-test') + alias('androidx-test-core-ktx').to('androidx.test', 'core-ktx').versionRef('androidx-test') alias('androidx-test-ext-junit').to('androidx.test.ext:junit:1.1.1') alias('androidx-test-ext-junit-ktx').to('androidx.test.ext:junit-ktx:1.1.1') - alias('espresso-core').to('androidx.test.espresso:espresso-core:3.2.0') + alias('espresso-core').to('androidx.test.espresso:espresso-core:3.4.0') alias('mockito-core').to('org.mockito:mockito-inline:4.4.0') alias('mockito-kotlin').to('org.mockito.kotlin:mockito-kotlin:4.0.0') alias('robolectric-robolectric').to('org.robolectric', 'robolectric').versionRef('robolectric') diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index a76dbc207..ec09a068a 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -24,6 +24,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -45,11 +53,17 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + @@ -71,6 +85,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -92,31 +114,49 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + @@ -162,61 +202,107 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -230,6 +316,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -243,16 +332,25 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + @@ -274,36 +372,57 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + @@ -325,46 +444,78 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -386,11 +537,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + @@ -404,6 +566,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -425,6 +590,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -446,11 +614,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + @@ -472,16 +651,30 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + @@ -511,11 +704,17 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + @@ -529,16 +728,25 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + @@ -624,16 +832,30 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + @@ -647,6 +869,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -676,71 +901,118 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + - - - - + + - - - + + + + + + + + + + + + + + - - + + - - - + + + + + + - - - - + + - - - + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -754,41 +1026,70 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -839,11 +1140,17 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + @@ -853,9 +1160,12 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + + + + @@ -886,6 +1196,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -899,6 +1212,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -912,16 +1228,25 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + @@ -931,20 +1256,21 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - + + + + + + @@ -958,6 +1284,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -971,6 +1300,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -984,6 +1316,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -997,6 +1332,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1010,6 +1348,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1023,6 +1364,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1047,11 +1391,17 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + @@ -1097,6 +1447,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1110,6 +1463,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1123,6 +1479,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1144,6 +1503,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1157,21 +1519,33 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + @@ -1185,6 +1559,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1198,6 +1575,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1211,6 +1591,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1224,6 +1607,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1237,6 +1623,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1250,6 +1639,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1263,6 +1655,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1292,6 +1687,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1321,26 +1719,86 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1354,6 +1812,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1367,6 +1828,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1380,6 +1844,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -1393,76 +1860,136 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1508,76 +2035,121 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1591,201 +2163,492 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1805,86 +2668,154 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1978,21 +2909,38 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + @@ -2014,176 +2962,366 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2197,126 +3335,206 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2330,6 +3548,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -2343,736 +3564,1422 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3178,6 +5085,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + @@ -3191,11 +5101,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + @@ -3246,206 +5167,362 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + - - - + + + + + + + + + - - - + + + + + + + + + - - - + + + + + + + + + - - - + + + + + + + + + + + - - - - + + - - - + + + + + + - - - + + + + + + - - - + + + + + + - - - + + + + + + - - - + + + + + + - - - + + + + + + - - - + + + + + + - - - + + + + + + - - - + + + + + + - - - + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3467,71 +5544,138 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +