kopia lustrzana https://github.com/ryukoposting/Signal-Android
Implement first-time-nav screen for stories.
rodzic
858c7a7f2e
commit
521bd2cce4
|
@ -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<String> = mutableListOf(MANUAL_FEATURE_DISABLE, USER_HAS_ADDED_TO_A_STORY, VIDEO_TOOLTIP_SEEN_MARKER, CANNOT_SEND_SEEN_MARKER)
|
||||
override fun getKeysToIncludeInBackup(): MutableList<String> = 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<StorySend> = getList(LATEST_STORY_SENDS, StorySendSerializer)
|
||||
|
|
|
@ -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<Drawable> {
|
||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
|
||||
setBlurHash(null)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, 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()
|
||||
}
|
||||
}
|
|
@ -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<StoryFirstTimeNavigationView>(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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="72dp"
|
||||
android:height="72dp"
|
||||
android:viewportWidth="72"
|
||||
android:viewportHeight="72">
|
||||
<path
|
||||
android:pathData="M38.773,12.241H54.773C57.964,12.241 60.551,14.851 60.551,18.069V53.931C60.551,57.15 57.964,59.759 54.773,59.759H38.773C36.699,59.759 34.879,58.656 33.86,57H31.372C32.569,59.934 35.432,62 38.773,62H54.773C59.191,62 62.773,58.387 62.773,53.931V18.069C62.773,13.613 59.191,10 54.773,10H38.773C34.714,10 31.361,13.049 30.843,17H33.092C33.59,14.292 35.944,12.241 38.773,12.241Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M55.131,28l-0,16l-16,0l-0,-16z"/>
|
||||
<path
|
||||
android:pathData="M47.646,41.514L46.585,40.453L49.456,37.583L50.581,36.78L38.131,36.78L38.131,35.28L50.581,35.28L49.54,34.536L46.582,31.542L47.649,30.488L53.128,36.033L47.646,41.514Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M53.481,36.032L47.646,41.866L46.232,40.451L49.294,37.39L49.801,37.028L37.881,37.028L37.881,35.028L49.801,35.028L49.377,34.725L46.229,31.538L47.651,30.133L53.481,36.032ZM47.647,30.84L46.936,31.542L49.703,34.343L51.361,35.528L38.381,35.528L38.381,36.528L51.362,36.528L49.618,37.773L46.939,40.451L47.646,41.159L52.776,36.03L47.647,30.84Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M31.373,24.166C31.639,24.466 31.845,24.815 31.976,25.194C32.108,25.573 32.164,25.975 32.141,26.375C32.118,26.776 32.015,27.168 31.841,27.529C31.666,27.89 31.422,28.213 31.123,28.479C31.123,28.48 31.122,28.48 31.122,28.481L28.154,31.142C28.481,31.313 28.775,31.542 29.021,31.818C29.288,32.118 29.493,32.467 29.625,32.846C29.757,33.225 29.812,33.627 29.789,34.027C29.766,34.428 29.664,34.82 29.489,35.181C29.429,35.305 29.361,35.424 29.285,35.538C29.635,35.709 29.953,35.947 30.218,36.243C30.759,36.847 31.038,37.64 30.994,38.45C30.962,39.03 30.766,39.584 30.435,40.051C30.79,40.223 31.112,40.465 31.379,40.766C31.918,41.371 32.194,42.166 32.147,42.975C32.1,43.784 31.734,44.541 31.128,45.08C31.121,45.087 31.114,45.093 31.106,45.099L30.599,45.525L30.172,45.903C29.898,46.146 29.511,46.489 29.061,46.888C28.161,47.688 27.011,48.711 26.007,49.612C25.963,49.651 25.917,49.693 25.87,49.736C24.854,50.651 22.953,52.364 20.465,53.036C19.135,53.395 17.636,53.459 16.024,52.97C14.417,52.482 12.757,51.463 11.068,49.746C9.342,47.991 8.376,46.117 7.988,44.145C7.603,42.187 7.8,40.197 8.295,38.219C8.788,36.246 9.591,34.237 10.457,32.235C10.734,31.594 11.016,30.957 11.298,30.32C11.907,28.945 12.516,27.571 13.071,26.167C13.631,24.603 15.219,24.061 16.475,24.459C17.12,24.664 17.707,25.12 18.034,25.817C18.361,26.513 18.382,27.343 18.108,28.225C17.597,29.919 17.119,31.695 16.749,33.133L16.857,33.043L27.056,23.917L27.058,23.916C27.358,23.649 27.707,23.444 28.087,23.312C28.465,23.18 28.867,23.124 29.267,23.147C29.668,23.171 30.06,23.273 30.421,23.447C30.783,23.622 31.106,23.867 31.373,24.166ZM15.041,35.872C14.068,35.644 14.068,35.644 14.068,35.644L14.079,35.595L14.112,35.454C14.141,35.332 14.184,35.153 14.239,34.927C14.349,34.474 14.507,33.832 14.7,33.071C15.087,31.551 15.619,29.551 16.195,27.644L16.197,27.635C16.359,27.115 16.293,26.815 16.224,26.667C16.154,26.519 16.033,26.417 15.871,26.366C15.519,26.254 15.106,26.404 14.951,26.849C14.946,26.861 14.942,26.873 14.937,26.885C14.365,28.334 13.726,29.777 13.107,31.176C12.829,31.804 12.555,32.422 12.293,33.029C11.434,35.014 10.687,36.898 10.235,38.705C9.784,40.506 9.641,42.183 9.95,43.759C10.257,45.319 11.022,46.847 12.494,48.343C14.002,49.876 15.383,50.685 16.605,51.056C17.819,51.425 18.934,51.377 19.944,51.105C21.958,50.561 23.547,49.134 24.584,48.202C24.614,48.175 24.643,48.149 24.671,48.124C25.678,47.22 26.832,46.193 27.733,45.393C28.183,44.993 28.571,44.649 28.846,44.405L29.283,44.018C29.29,44.013 29.297,44.007 29.303,44.001L29.808,43.578C30.011,43.392 30.135,43.134 30.151,42.859C30.167,42.579 30.071,42.305 29.885,42.096C29.699,41.886 29.437,41.76 29.158,41.743C28.878,41.727 28.604,41.823 28.394,42.009C28.337,42.06 28.275,42.103 28.211,42.139L25.892,44.206C25.48,44.574 24.848,44.537 24.48,44.125C24.113,43.713 24.149,43.081 24.561,42.713L28.647,39.07C28.647,39.069 28.647,39.07 28.647,39.07C28.855,38.883 28.982,38.62 28.997,38.341C29.012,38.061 28.916,37.787 28.729,37.578C28.542,37.37 28.279,37.244 28,37.229C27.72,37.214 27.446,37.31 27.237,37.497C27.19,37.539 27.141,37.576 27.089,37.607L23.621,40.649C23.206,41.013 22.574,40.972 22.21,40.556C21.846,40.141 21.887,39.509 22.302,39.145L27.443,34.637C27.545,34.545 27.629,34.434 27.689,34.31C27.749,34.185 27.784,34.05 27.792,33.911C27.801,33.773 27.781,33.634 27.736,33.503C27.69,33.372 27.619,33.251 27.527,33.148C27.435,33.044 27.323,32.96 27.198,32.9C27.074,32.839 26.938,32.804 26.8,32.796C26.661,32.788 26.523,32.807 26.392,32.853C26.261,32.898 26.14,32.969 26.036,33.061C25.993,33.1 25.947,33.134 25.899,33.164L20.923,37.626C20.512,37.995 19.879,37.96 19.511,37.549C19.142,37.138 19.177,36.506 19.588,36.137L29.789,26.989L29.792,26.987C29.896,26.895 29.98,26.783 30.041,26.658C30.101,26.533 30.136,26.398 30.144,26.259C30.152,26.121 30.133,25.982 30.087,25.851C30.042,25.72 29.971,25.6 29.879,25.496C29.787,25.392 29.675,25.308 29.55,25.248C29.425,25.187 29.29,25.152 29.151,25.144C29.013,25.136 28.874,25.155 28.743,25.201C28.613,25.246 28.492,25.317 28.389,25.409C28.389,25.409 28.388,25.409 28.388,25.409L18.179,34.544C18.174,34.548 18.17,34.551 18.166,34.555C18.162,34.558 18.159,34.561 18.155,34.565L15.684,36.638C15.354,36.914 14.885,36.949 14.518,36.724C14.151,36.498 13.97,36.063 14.068,35.644L15.041,35.872Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -0,0 +1,34 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="72dp"
|
||||
android:height="72dp"
|
||||
android:viewportWidth="72"
|
||||
android:viewportHeight="72">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h72v72h-72z"/>
|
||||
<path
|
||||
android:pathData="M21,9.241H37C40.191,9.241 42.778,11.851 42.778,15.069V34H45V15.069C45,10.613 41.418,7 37,7H21C16.582,7 13,10.613 13,15.069V50.931C13,55.387 16.582,59 21,59H26.642V56.759H21C17.809,56.759 15.222,54.15 15.222,50.931V15.069C15.222,11.851 17.809,9.241 21,9.241Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M21.358,23h16v16h-16z"/>
|
||||
<path
|
||||
android:pathData="M34.871,30.486L33.81,31.547L30.94,28.676L30.137,27.551V40.001H28.637V27.551L27.893,28.592L24.899,31.55L23.845,30.483L29.39,25.004L34.871,30.486Z"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
</group>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M19.358,56.125l21.16,-29.125l29.125,21.16l-21.16,29.125z"/>
|
||||
<path
|
||||
android:pathData="M47.663,51.466L44.216,45.556C44.078,45.324 43.896,45.121 43.68,44.959C43.464,44.798 43.218,44.68 42.956,44.613C42.695,44.546 42.423,44.532 42.155,44.57C41.888,44.608 41.631,44.699 41.399,44.837C41.167,44.974 40.964,45.157 40.802,45.373C40.64,45.589 40.522,45.835 40.456,46.096C40.389,46.358 40.374,46.63 40.413,46.897C40.451,47.164 40.542,47.422 40.679,47.654M51.592,49.922L48.794,45.214C48.514,44.746 48.06,44.408 47.531,44.275C47.002,44.142 46.442,44.225 45.974,44.505C45.506,44.784 45.168,45.239 45.035,45.768C44.902,46.296 44.985,46.856 45.265,47.324M44.186,53.547L37.153,41.788C37.016,41.555 36.833,41.353 36.617,41.191C36.401,41.029 36.155,40.911 35.894,40.845C35.632,40.778 35.36,40.763 35.093,40.801C34.826,40.84 34.569,40.93 34.336,41.068C34.104,41.206 33.901,41.388 33.739,41.604C33.578,41.82 33.46,42.066 33.393,42.328C33.326,42.589 33.312,42.861 33.35,43.129C33.388,43.396 33.479,43.653 33.617,43.885L40.637,55.652L42.2,58.473C42.2,58.473 38.299,56.725 34.81,54.886C32.219,53.492 30.969,56.621 32.821,57.703C40.358,62.513 46.908,69.224 54.509,64.187C62.11,59.15 58.31,53.162 56.923,50.844C55.535,48.526 53.776,45.539 53.776,45.539L53.449,44.949C53.171,44.48 52.718,44.14 52.189,44.006C51.661,43.871 51.101,43.951 50.632,44.229C50.163,44.507 49.824,44.96 49.689,45.489C49.554,46.017 49.634,46.577 49.912,47.046"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
File diff suppressed because one or more lines are too long
|
@ -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." />
|
||||
</FrameLayout>
|
||||
|
||||
|
@ -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" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/story_first_time_nav_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:inflatedId="@+id/story_first_time_navigation_view"
|
||||
android:layout="@layout/story_viewer_first_time_navigation_stub"
|
||||
app:layout_constraintBottom_toBottomOf="@id/story_content_card_touch_interceptor"
|
||||
app:layout_constraintTop_toTopOf="@id/story_content_card_touch_interceptor" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,111 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/edu_blur_hash"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/edu_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:src="@color/transparent_black_70"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/edu_tap_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintBottom_toTopOf="@id/edu_swipe_up_icon"
|
||||
app:layout_constraintEnd_toStartOf="@id/edu_tap_label"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_tap_72"
|
||||
app:tint="@color/core_white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/edu_tap_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/StoryFirstTimeNavigationView__tap_to_advance"
|
||||
android:textAppearance="@style/Signal.Text.BodyLarge"
|
||||
android:textColor="@color/core_white"
|
||||
app:layout_constraintBottom_toBottomOf="@id/edu_tap_icon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/edu_tap_icon"
|
||||
app:layout_constraintTop_toTopOf="@id/edu_tap_icon" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/edu_swipe_up_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintBottom_toTopOf="@id/edu_swipe_right_icon"
|
||||
app:layout_constraintEnd_toStartOf="@id/edu_swipe_up_label"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/edu_tap_icon"
|
||||
app:srcCompat="@drawable/ic_swipe_up_72"
|
||||
app:tint="@color/core_white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/edu_swipe_up_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/StoryFirstTimeNavigationView__swipe_up_to_skip"
|
||||
android:textAppearance="@style/Signal.Text.BodyLarge"
|
||||
android:textColor="@color/core_white"
|
||||
app:layout_constraintBottom_toBottomOf="@id/edu_swipe_up_icon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/edu_swipe_up_icon"
|
||||
app:layout_constraintTop_toTopOf="@id/edu_swipe_up_icon" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/edu_swipe_right_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintBottom_toTopOf="@id/edu_got_it"
|
||||
app:layout_constraintEnd_toStartOf="@id/edu_swipe_right_label"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/edu_swipe_up_icon"
|
||||
app:srcCompat="@drawable/ic_swipe_right_72"
|
||||
app:tint="@color/core_white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/edu_swipe_right_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/StoryFirstTimeNavigationView__swipe_right_to_exit"
|
||||
android:textAppearance="@style/Signal.Text.BodyLarge"
|
||||
android:textColor="@color/core_white"
|
||||
app:layout_constraintBottom_toBottomOf="@id/edu_swipe_right_icon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/edu_swipe_right_icon"
|
||||
app:layout_constraintTop_toTopOf="@id/edu_swipe_right_icon" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/edu_got_it"
|
||||
style="@style/Signal.Widget.Button.Medium.Tonal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/StoryFirstTimeNagivationView__got_it"
|
||||
android:textColor="@color/signal_colorNeutral"
|
||||
app:backgroundTint="@color/signal_colorNeutralInverse"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/edu_swipe_right_icon" />
|
||||
|
||||
</merge>
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.stories.StoryFirstTimeNavigationView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/story_content_card_touch_interceptor"
|
||||
app:layout_constraintTop_toTopOf="@id/story_content_card_touch_interceptor" />
|
|
@ -4903,6 +4903,15 @@
|
|||
<!-- Gift expiry expired -->
|
||||
<string name="Gifts__expired">Expired</string>
|
||||
|
||||
<!-- Label indicating that a user can tap to advance to the next post in a story -->
|
||||
<string name="StoryFirstTimeNavigationView__tap_to_advance">Tap to advance</string>
|
||||
<!-- Label indicating swipe direction to skip current story -->
|
||||
<string name="StoryFirstTimeNavigationView__swipe_up_to_skip">Swipe up to skip</string>
|
||||
<!-- Label indicating swipe direction to exit story viewer -->
|
||||
<string name="StoryFirstTimeNavigationView__swipe_right_to_exit">Swipe right to exit</string>
|
||||
<!-- Button label to confirm understanding of story navigation -->
|
||||
<string name="StoryFirstTimeNagivationView__got_it">Got it</string>
|
||||
|
||||
<!-- EOF -->
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -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<GlideApp>
|
||||
|
||||
@Mock
|
||||
private lateinit var glideRequests: GlideRequests
|
||||
|
||||
@Mock
|
||||
private lateinit var glideRequest: GlideRequest<Drawable>
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
testSubject = StoryFirstTimeNavigationView(ContextThemeWrapper(ApplicationProvider.getApplicationContext(), org.thoughtcrime.securesms.R.style.Signal_DayNight))
|
||||
|
||||
whenever(GlideApp.with(any<View>())).thenReturn(glideRequests)
|
||||
whenever(glideRequests.load(any<BlurHash>())).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<View>(org.thoughtcrime.securesms.R.id.edu_overlay).visible)
|
||||
assertFalse(testSubject.findViewById<View>(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<View>(org.thoughtcrime.securesms.R.id.edu_overlay).visible)
|
||||
assertTrue(testSubject.findViewById<View>(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<View>(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<ImageView>(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<ImageView>(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<ImageView>(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)
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
|
|
Plik diff jest za duży
Load Diff
Ładowanie…
Reference in New Issue