diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferControlView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferControlView.kt index 31142d752c..166b518820 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferControlView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferControlView.kt @@ -13,9 +13,6 @@ import android.view.LayoutInflater import android.view.View import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.children -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -29,25 +26,20 @@ import org.thoughtcrime.securesms.events.PartProgressEvent import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.ViewUtil -import org.thoughtcrime.securesms.util.rx.RxStore import org.thoughtcrime.securesms.util.visible +import java.util.UUID class TransferControlView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) { + private val uuid = UUID.randomUUID().toString() private val binding: TransferControlsViewBinding - private val store = RxStore(TransferControlViewState()) - private val disposables = CompositeDisposable().apply { - add(store) - } - private var previousState = TransferControlViewState() + private var state = TransferControlViewState() init { + tag = uuid binding = TransferControlsViewBinding.inflate(LayoutInflater.from(context), this) visibility = GONE isLongClickable = false - disposables += store.stateFlowable.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { - applyState(it) - } addOnAttachStateChangeListener(RecyclerViewParentTransitionController(child = this)) } @@ -60,11 +52,20 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att override fun onDetachedFromWindow() { super.onDetachedFromWindow() EventBus.getDefault().unregister(this) - disposables.clear() + } + + private fun updateState(stateFactory: (TransferControlViewState) -> TransferControlViewState) { + val newState = stateFactory.invoke(state) + if (newState != state) { + applyState(newState) + } + state = newState } private fun applyState(currentState: TransferControlViewState) { - when (deriveMode(currentState)) { + val mode = deriveMode(currentState) + verboseLog("New state applying, mode = $mode") + when (mode) { Mode.PENDING_GALLERY -> displayPendingGallery(currentState) Mode.PENDING_GALLERY_CONTAINS_PLAYABLE -> displayPendingGalleryWithPlayable(currentState) Mode.PENDING_SINGLE_ITEM -> displayPendingSingleItem(currentState) @@ -76,18 +77,18 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att Mode.UPLOADING_SINGLE_ITEM -> displayUploadingSingleItem(currentState) Mode.RETRY_DOWNLOADING -> displayRetry(currentState, false) Mode.RETRY_UPLOADING -> displayRetry(currentState, true) - Mode.GONE -> displayChildrenAsGone(currentState, previousState) + Mode.GONE -> displayChildrenAsGone() } - - previousState = currentState } private fun deriveMode(currentState: TransferControlViewState): Mode { if (currentState.slides.isEmpty()) { + verboseLog("Setting empty slide deck to GONE") return Mode.GONE } if (currentState.slides.all { it.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE }) { + verboseLog("Setting slide deck that's finished to GONE\n\t${slidesAsListOfTimestamps(currentState.slides)}") return Mode.GONE } @@ -190,15 +191,17 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att } AttachmentTable.TRANSFER_PROGRESS_DONE -> { + verboseLog("[Case 2] Setting slide deck that's finished to GONE\t${slidesAsListOfTimestamps(currentState.slides)}") return Mode.GONE } } } } else { + verboseLog("Setting slide deck to GONE because isVisible is false:\t${slidesAsListOfTimestamps(currentState.slides)}") return Mode.GONE } - Log.i(TAG, "Hit default mode case, this should not happen.") + Log.i(TAG, "[$uuid] Hit default mode case, this should not happen.") return Mode.GONE } @@ -212,6 +215,11 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att secondaryDetailsText = currentState.showSecondaryText ) + binding.primaryDetailsText.translationX = if (ViewUtil.isLtr(this)) { + ViewUtil.dpToPx(-8).toFloat() + } else { + ViewUtil.dpToPx(8).toFloat() + } val remainingSlides = currentState.slides.filterNot { it.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE } val downloadCount = remainingSlides.size binding.primaryDetailsText.text = context.resources.getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount) @@ -374,7 +382,7 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att binding.secondaryDetailsText.text = resources.getString(R.string.NetworkFailure__retry) } - private fun displayChildrenAsGone(currentState: TransferControlViewState, prevState: TransferControlViewState) { + private fun displayChildrenAsGone() { children.forEach { it.visible = false } @@ -427,37 +435,44 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att override fun setFocusable(focusable: Boolean) { super.setFocusable(false) - store.update { it.copy(isFocusable = focusable) } + verboseLog("setFocusable update: $focusable") + updateState { it.copy(isFocusable = focusable) } } override fun setClickable(clickable: Boolean) { super.setClickable(false) - store.update { it.copy(isClickable = clickable) } + verboseLog("setClickable update: $clickable") + updateState { it.copy(isClickable = clickable) } } @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) fun onEventAsync(event: PartProgressEvent) { val attachment = event.attachment - store.update { + updateState { + verboseLog("onEventAsync update") if (!it.networkProgress.containsKey(attachment)) { - return@update it + verboseLog("onEventAsync update ignored") + return@updateState it } if (event.type == PartProgressEvent.Type.COMPRESSION) { val mutableMap = it.compressionProgress.toMutableMap() mutableMap[attachment] = Progress.fromEvent(event) - return@update it.copy(compressionProgress = mutableMap.toMap()) + verboseLog("onEventAsync compression update") + return@updateState it.copy(compressionProgress = mutableMap.toMap()) } else { val mutableMap = it.networkProgress.toMutableMap() mutableMap[attachment] = Progress.fromEvent(event) - return@update it.copy(networkProgress = mutableMap.toMap()) + verboseLog("onEventAsync network update") + return@updateState it.copy(networkProgress = mutableMap.toMap()) } } } fun setSlides(slides: List) { - require(slides.isNotEmpty()) { "Must provide at least one slide." } - store.update { state -> + require(slides.isNotEmpty()) { "[$uuid] Must provide at least one slide." } + updateState { state -> + verboseLog("State update for new slides: ${slidesAsListOfTimestamps(slides)}") val isNewSlideSet = !isUpdateToExistingSet(state, slides) val networkProgress: MutableMap = if (isNewSlideSet) HashMap() else state.networkProgress.toMutableMap() if (isNewSlideSet) { @@ -476,14 +491,25 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att val playableWhileDownloading = allStreamableOrDone val isOutgoing = slides.any { it.asAttachment().uploadTimestamp == 0L } - state.copy( + val result = state.copy( slides = slides, networkProgress = networkProgress, compressionProgress = compressionProgress, playableWhileDownloading = playableWhileDownloading, isOutgoing = isOutgoing ) + verboseLog("New state calculated and being returned for new slides: ${slidesAsListOfTimestamps(slides)}\n$result") + return@updateState result } + verboseLog("End of setSlides() for ${slidesAsListOfTimestamps(slides)}") + } + + private fun slidesAsListOfTimestamps(slides: List): String { + if (!VERBOSE_DEVELOPMENT_LOGGING) { + return "" + } + + return slides.map { it.asAttachment().uploadTimestamp }.joinToString() } private fun isUpdateToExistingSet(currentState: TransferControlViewState, slides: List): Boolean { @@ -499,38 +525,53 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att } fun setDownloadClickListener(listener: OnClickListener) { - store.update { - it.copy(downloadClickedListener = listener) + verboseLog("downloadClickListener update") + updateState { + it.copy( + downloadClickedListener = listener + ) } } fun setCancelClickListener(listener: OnClickListener) { - store.update { - it.copy(cancelDownloadClickedListener = listener) + verboseLog("cancelClickListener update") + updateState { + it.copy( + cancelDownloadClickedListener = listener + ) } } fun setInstantPlaybackClickListener(listener: OnClickListener) { - store.update { - it.copy(instantPlaybackClickListener = listener) + verboseLog("instantPlaybackClickListener update") + updateState { + it.copy( + instantPlaybackClickListener = listener + ) } } fun clear() { clearAnimation() visibility = GONE - store.update { TransferControlViewState() } + updateState { TransferControlViewState() } } fun setShowSecondaryText(showSecondaryText: Boolean) { - store.update { - it.copy(showSecondaryText = showSecondaryText) + verboseLog("showSecondaryText update: $showSecondaryText") + updateState { + it.copy( + showSecondaryText = showSecondaryText + ) } } fun setVisible(isVisible: Boolean) { - store.update { - it.copy(isVisible = isVisible) + verboseLog("showSecondaryText update: $isVisible") + updateState { + it.copy( + isVisible = isVisible + ) } } @@ -558,12 +599,22 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att } } + /** + * This is an extremely chatty logging mode for local development. Each view is assigned a UUID so that you can filter by view inside a conversation. + */ + private fun verboseLog(message: String) { + if (VERBOSE_DEVELOPMENT_LOGGING) { + Log.d(TAG, "[$uuid] $message") + } + } + companion object { private const val TAG = "TransferControlView" + private const val VERBOSE_DEVELOPMENT_LOGGING = false private const val UPLOAD_TASK_WEIGHT = 1 /** - * A weighting compared to [.UPLOAD_TASK_WEIGHT] + * A weighting compared to [UPLOAD_TASK_WEIGHT] */ private const val COMPRESSION_TASK_WEIGHT = 3 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferProgressView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferProgressView.kt index ed9983db25..37467b7233 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferProgressView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferProgressView.kt @@ -6,6 +6,7 @@ package org.thoughtcrime.securesms.components.transfercontrols import android.content.Context +import android.content.res.Resources import android.graphics.Canvas import android.graphics.Paint import android.graphics.PorterDuff @@ -16,7 +17,6 @@ import android.util.AttributeSet import android.view.View import androidx.core.content.ContextCompat import androidx.core.graphics.withTranslation -import org.signal.core.util.dp import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import kotlin.math.roundToInt @@ -32,28 +32,55 @@ class TransferProgressView @JvmOverloads constructor( ) : View(context, attrs, defStyleAttr, defStyleRes) { companion object { const val TAG = "TransferProgressView" - private const val PROGRESS_ARC_STROKE_WIDTH = 1.5f - private const val ICON_INSET_PERCENT = 0.2f + private const val PROGRESS_ARC_STROKE_WIDTH_DP = 1.5f + private const val ICON_SIZE_DP = 24f + private const val STOP_CORNER_RADIUS_DP = 4f + private const val PROGRESS_BAR_INSET_DP = 3 } + private val iconColor: Int + private val progressColor: Int + private val trackColor: Int + private val stopIconPaint: Paint + private val progressPaint: Paint + private val trackPaint: Paint + private val progressArcStrokeWidth: Float + private val iconSize: Float + private val stopIconSize: Float + private val stopIconCornerRadius: Float + private val progressRect = RectF() private val stopIconRect = RectF() - private val progressPaint = progressPaint() - private val stopIconPaint = stopIconPaint() - private val trackPaint = trackPaint() + private val downloadDrawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down_24) + private val uploadDrawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_up_16) private var progressPercent = 0f private var currentState = State.UNINITIALIZED - private val downloadDrawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down_24) - private val uploadDrawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_up_16) - var startClickListener: OnClickListener? = null var cancelClickListener: OnClickListener? = null init { - val tint = ContextCompat.getColor(context, R.color.signal_colorOnCustom) - val filter = PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_ATOP) + val displayDensity = Resources.getSystem().displayMetrics.density + val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.TransferProgressView, 0, 0) + val signalCustomColor = ContextCompat.getColor(context, R.color.signal_colorOnCustom) + val signalTransparent2 = ContextCompat.getColor(context, R.color.signal_colorTransparent2) + + iconColor = typedArray.getColor(R.styleable.TransferProgressView_transferIconColor, signalCustomColor) + progressColor = typedArray.getColor(R.styleable.TransferProgressView_progressColor, signalCustomColor) + trackColor = typedArray.getColor(R.styleable.TransferProgressView_trackColor, signalTransparent2) + progressArcStrokeWidth = typedArray.getDimension(R.styleable.TransferProgressView_progressArcWidth, PROGRESS_ARC_STROKE_WIDTH_DP * displayDensity) + iconSize = typedArray.getDimension(R.styleable.TransferProgressView_iconSize, ICON_SIZE_DP * displayDensity) + stopIconSize = typedArray.getDimension(R.styleable.TransferProgressView_stopIconSize, ICON_SIZE_DP * displayDensity) + stopIconCornerRadius = typedArray.getDimension(R.styleable.TransferProgressView_stopIconCornerRadius, STOP_CORNER_RADIUS_DP * displayDensity) + + typedArray.recycle() + + progressPaint = progressPaint(progressColor) + stopIconPaint = stopIconPaint(iconColor) + trackPaint = trackPaint(trackColor) + + val filter = PorterDuffColorFilter(iconColor, PorterDuff.Mode.SRC_ATOP) downloadDrawable?.colorFilter = filter uploadDrawable?.colorFilter = filter } @@ -100,49 +127,46 @@ class TransferProgressView @JvmOverloads constructor( } private fun drawProgress(canvas: Canvas, progressPercent: Float) { - val miniIcon = height < 32.dp - val stopIconCornerRadius = if (miniIcon) 1f.dp else 4f.dp - val iconSize: Float = if (miniIcon) 5.5f.dp else 16f.dp - stopIconRect.set(0f, 0f, iconSize, iconSize) + stopIconRect.set(0f, 0f, stopIconSize, stopIconSize) - canvas.withTranslation(width / 2 - (iconSize / 2), height / 2 - (iconSize / 2)) { + canvas.withTranslation(width / 2 - (stopIconSize / 2), height / 2 - (stopIconSize / 2)) { drawRoundRect(stopIconRect, stopIconCornerRadius, stopIconCornerRadius, stopIconPaint) } - val widthDp = PROGRESS_ARC_STROKE_WIDTH.dp - val inset = 2.dp - progressRect.top = widthDp + inset - progressRect.left = widthDp + inset - progressRect.right = (width - widthDp) - inset - progressRect.bottom = (height - widthDp) - inset + val trackWidthScaled = progressArcStrokeWidth + val inset: Float = PROGRESS_BAR_INSET_DP * Resources.getSystem().displayMetrics.density + progressRect.left = trackWidthScaled + inset + progressRect.top = trackWidthScaled + inset + progressRect.right = (width - trackWidthScaled) - inset + progressRect.bottom = (height - trackWidthScaled) - inset canvas.drawArc(progressRect, 0f, 360f, false, trackPaint) canvas.drawArc(progressRect, 270f, 360f * progressPercent, false, progressPaint) } - private fun stopIconPaint(): Paint { + private fun stopIconPaint(paintColor: Int): Paint { val stopIconPaint = Paint() - stopIconPaint.color = ContextCompat.getColor(context, R.color.signal_colorOnCustom) + stopIconPaint.color = paintColor stopIconPaint.isAntiAlias = true stopIconPaint.style = Paint.Style.FILL return stopIconPaint } - private fun trackPaint(): Paint { + private fun trackPaint(trackColor: Int): Paint { val trackPaint = Paint() - trackPaint.color = ContextCompat.getColor(context, R.color.signal_colorTransparent2) + trackPaint.color = trackColor trackPaint.isAntiAlias = true trackPaint.style = Paint.Style.STROKE - trackPaint.strokeWidth = PROGRESS_ARC_STROKE_WIDTH.dp + trackPaint.strokeWidth = progressArcStrokeWidth return trackPaint } - private fun progressPaint(): Paint { + private fun progressPaint(progressColor: Int): Paint { val progressPaint = Paint() - progressPaint.color = ContextCompat.getColor(context, R.color.signal_colorOnCustom) + progressPaint.color = progressColor progressPaint.isAntiAlias = true progressPaint.style = Paint.Style.STROKE - progressPaint.strokeWidth = PROGRESS_ARC_STROKE_WIDTH.dp + progressPaint.strokeWidth = progressArcStrokeWidth return progressPaint } @@ -152,13 +176,18 @@ class TransferProgressView @JvmOverloads constructor( return } - drawable.setBounds( - (width * ICON_INSET_PERCENT).roundToInt(), - (height * ICON_INSET_PERCENT).roundToInt(), - (width * (1 - ICON_INSET_PERCENT)).roundToInt(), - (height * (1 - ICON_INSET_PERCENT)).roundToInt() - ) + val centerX = width / 2f + val centerY = height / 2f + // 0, 0 is the top left corner + // width, height is the bottom right + val halfIconSize = (iconSize / 2f) + val left = (centerX - halfIconSize).roundToInt().coerceAtLeast(0) + val top = (centerY - halfIconSize).roundToInt().coerceAtLeast(0) + val right = (centerX + halfIconSize).roundToInt().coerceAtMost(width) + val bottom = (centerY + halfIconSize).roundToInt().coerceAtMost(height) + + drawable.setBounds(left, top, right, bottom) drawable.draw(canvas) } diff --git a/app/src/main/res/drawable/ic_arrow_up_16.xml b/app/src/main/res/drawable/ic_arrow_up_16.xml index c3b4dd505a..da393d508d 100644 --- a/app/src/main/res/drawable/ic_arrow_up_16.xml +++ b/app/src/main/res/drawable/ic_arrow_up_16.xml @@ -5,5 +5,5 @@ android:viewportHeight="16"> - + android:pathData="M8 2.38c-0.2 0-0.39 0.07-0.53 0.21L3.22 6.84c-0.3 0.3-0.3 0.77 0 1.07 0.3 0.29 0.77 0.29 1.06 0l3.05-3.05-0.08 1.26v7c0 0.42 0.34 0.76 0.75 0.76s0.75-0.34 0.75-0.76v-7L8.67 4.87l3.05 3.05c0.3 0.29 0.77 0.29 1.06 0 0.3-0.3 0.3-0.77 0-1.07L8.53 2.6C8.39 2.45 8.2 2.38 8 2.38Z"/> + \ No newline at end of file diff --git a/app/src/main/res/layout/transfer_controls_view.xml b/app/src/main/res/layout/transfer_controls_view.xml index 2783f7b075..e76f73df5d 100644 --- a/app/src/main/res/layout/transfer_controls_view.xml +++ b/app/src/main/res/layout/transfer_controls_view.xml @@ -37,13 +37,16 @@ @@ -52,7 +55,7 @@ style="@style/Signal.Text.Caption" android:includeFontPadding="false" android:layout_width="wrap_content" - android:layout_height="24dp" + android:layout_height="26dp" android:layout_marginStart="@dimen/transfer_control_view_progressbar_to_textview_margin" android:layout_marginEnd="4dp" android:fontFamily="sans-serif-light" @@ -71,15 +74,16 @@ android:id="@+id/primary_progress_view" android:layout_width="@dimen/transfer_control_view_primary_background_height" android:layout_height="@dimen/transfer_control_view_primary_background_height" + app:iconSize="24dp" + app:stopIconSize="14dp" + app:stopIconCornerRadius="2dp" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@+id/primary_details_text" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - + + + + + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index f7a7ba5cc3..97685fe6fb 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -159,7 +159,7 @@ 32dp 24dp 4dp - 44dp + 50dp 80dp 70dp