jonnyandrew
11 months ago
committed by
GitHub
293 changed files with 967 additions and 52 deletions
@ -0,0 +1,25 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.voicemessages |
||||||
|
|
||||||
|
import io.element.android.libraries.textcomposer.model.PressEvent |
||||||
|
|
||||||
|
sealed class VoiceMessageComposerEvents { |
||||||
|
data class RecordButtonEvent( |
||||||
|
val pressEvent: PressEvent |
||||||
|
): VoiceMessageComposerEvents() |
||||||
|
} |
@ -0,0 +1,64 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.voicemessages |
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.runtime.getValue |
||||||
|
import androidx.compose.runtime.mutableStateOf |
||||||
|
import androidx.compose.runtime.remember |
||||||
|
import androidx.compose.runtime.setValue |
||||||
|
import io.element.android.libraries.architecture.Presenter |
||||||
|
import io.element.android.libraries.di.RoomScope |
||||||
|
import io.element.android.libraries.di.SingleIn |
||||||
|
import io.element.android.libraries.textcomposer.model.PressEvent |
||||||
|
import io.element.android.libraries.textcomposer.model.VoiceMessageState |
||||||
|
import javax.inject.Inject |
||||||
|
|
||||||
|
@SingleIn(RoomScope::class) |
||||||
|
class VoiceMessageComposerPresenter @Inject constructor() : Presenter<VoiceMessageComposerState> { |
||||||
|
@Composable |
||||||
|
override fun present(): VoiceMessageComposerState { |
||||||
|
var voiceMessageState by remember { mutableStateOf<VoiceMessageState>(VoiceMessageState.Idle) } |
||||||
|
|
||||||
|
fun onRecordButtonPress(event: VoiceMessageComposerEvents.RecordButtonEvent) = when(event.pressEvent) { |
||||||
|
PressEvent.PressStart -> { |
||||||
|
// TODO start the recording |
||||||
|
voiceMessageState = VoiceMessageState.Recording |
||||||
|
} |
||||||
|
PressEvent.LongPressEnd -> { |
||||||
|
// TODO finish the recording |
||||||
|
voiceMessageState = VoiceMessageState.Idle |
||||||
|
} |
||||||
|
PressEvent.Tapped -> { |
||||||
|
// TODO discard the recording and show the 'hold to record' tooltip |
||||||
|
voiceMessageState = VoiceMessageState.Idle |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
fun handleEvents(event: VoiceMessageComposerEvents) { |
||||||
|
when (event) { |
||||||
|
is VoiceMessageComposerEvents.RecordButtonEvent -> onRecordButtonPress(event) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return VoiceMessageComposerState( |
||||||
|
voiceMessageState = voiceMessageState, |
||||||
|
eventSink = { handleEvents(it) } |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.voicemessages |
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable |
||||||
|
import io.element.android.libraries.textcomposer.model.VoiceMessageState |
||||||
|
|
||||||
|
@Stable |
||||||
|
data class VoiceMessageComposerState( |
||||||
|
val voiceMessageState: VoiceMessageState, |
||||||
|
val eventSink: (VoiceMessageComposerEvents) -> Unit, |
||||||
|
) |
||||||
|
|
@ -0,0 +1,34 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.voicemessages |
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider |
||||||
|
import io.element.android.libraries.textcomposer.model.VoiceMessageState |
||||||
|
|
||||||
|
internal open class VoiceMessageComposerStateProvider : PreviewParameterProvider<VoiceMessageComposerState> { |
||||||
|
override val values: Sequence<VoiceMessageComposerState> |
||||||
|
get() = sequenceOf( |
||||||
|
aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording), |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
internal fun aVoiceMessageComposerState( |
||||||
|
voiceMessageState: VoiceMessageState = VoiceMessageState.Idle, |
||||||
|
) = VoiceMessageComposerState( |
||||||
|
voiceMessageState = voiceMessageState, |
||||||
|
eventSink = {}, |
||||||
|
) |
@ -0,0 +1,89 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class) |
||||||
|
|
||||||
|
package io.element.android.features.messages.voicemessages |
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionMode |
||||||
|
import app.cash.molecule.moleculeFlow |
||||||
|
import app.cash.turbine.test |
||||||
|
import com.google.common.truth.Truth.assertThat |
||||||
|
import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerEvents |
||||||
|
import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerPresenter |
||||||
|
import io.element.android.libraries.textcomposer.model.PressEvent |
||||||
|
import io.element.android.libraries.textcomposer.model.VoiceMessageState |
||||||
|
import io.element.android.tests.testutils.WarmUpRule |
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi |
||||||
|
import kotlinx.coroutines.test.runTest |
||||||
|
import org.junit.Rule |
||||||
|
import org.junit.Test |
||||||
|
|
||||||
|
class VoiceMessageComposerPresenterTest { |
||||||
|
|
||||||
|
@get:Rule |
||||||
|
val warmUpRule = WarmUpRule() |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - initial state`() = runTest { |
||||||
|
val presenter = createPresenter() |
||||||
|
moleculeFlow(RecompositionMode.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - recording state`() = runTest { |
||||||
|
val presenter = createPresenter() |
||||||
|
moleculeFlow(RecompositionMode.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) |
||||||
|
|
||||||
|
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Recording) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - abort recording`() = runTest { |
||||||
|
val presenter = createPresenter() |
||||||
|
moleculeFlow(RecompositionMode.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) |
||||||
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.Tapped)) |
||||||
|
|
||||||
|
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - finish recording`() = runTest { |
||||||
|
val presenter = createPresenter() |
||||||
|
moleculeFlow(RecompositionMode.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) |
||||||
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) |
||||||
|
|
||||||
|
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) |
||||||
|
} |
||||||
|
} |
||||||
|
private fun createPresenter() = VoiceMessageComposerPresenter() |
||||||
|
} |
@ -0,0 +1,108 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.libraries.textcomposer.components |
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row |
||||||
|
import androidx.compose.foundation.layout.size |
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.runtime.rememberCoroutineScope |
||||||
|
import androidx.compose.ui.Modifier |
||||||
|
import androidx.compose.ui.input.pointer.PointerEventType |
||||||
|
import androidx.compose.ui.input.pointer.pointerInput |
||||||
|
import androidx.compose.ui.res.stringResource |
||||||
|
import androidx.compose.ui.unit.dp |
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview |
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight |
||||||
|
import io.element.android.libraries.designsystem.text.applyScaleUp |
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon |
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconButton |
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables |
||||||
|
import io.element.android.libraries.textcomposer.utils.PressState |
||||||
|
import io.element.android.libraries.textcomposer.utils.PressStateEffects |
||||||
|
import io.element.android.libraries.textcomposer.utils.rememberPressState |
||||||
|
import io.element.android.libraries.theme.ElementTheme |
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings |
||||||
|
import kotlinx.coroutines.launch |
||||||
|
|
||||||
|
@Composable |
||||||
|
internal fun RecordButton( |
||||||
|
modifier: Modifier = Modifier, |
||||||
|
onPressStart: () -> Unit = {}, |
||||||
|
onLongPressEnd: () -> Unit = {}, |
||||||
|
onTap: () -> Unit = {}, |
||||||
|
) { |
||||||
|
val coroutineScope = rememberCoroutineScope() |
||||||
|
val pressState = rememberPressState() |
||||||
|
|
||||||
|
PressStateEffects( |
||||||
|
pressState = pressState.value, |
||||||
|
onPressStart = onPressStart, |
||||||
|
onLongPressEnd = onLongPressEnd, |
||||||
|
onTap = onTap, |
||||||
|
) |
||||||
|
|
||||||
|
RecordButtonView( |
||||||
|
isPressed = pressState.value is PressState.Pressing, |
||||||
|
modifier = modifier |
||||||
|
.pointerInput(Unit) { |
||||||
|
awaitPointerEventScope { |
||||||
|
while (true) { |
||||||
|
val event = awaitPointerEvent() |
||||||
|
coroutineScope.launch { |
||||||
|
when (event.type) { |
||||||
|
PointerEventType.Press -> pressState.press() |
||||||
|
PointerEventType.Release -> pressState.release() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
private fun RecordButtonView( |
||||||
|
isPressed: Boolean, |
||||||
|
modifier: Modifier = Modifier, |
||||||
|
) { |
||||||
|
IconButton( |
||||||
|
modifier = modifier |
||||||
|
.size(48.dp), |
||||||
|
onClick = {}, |
||||||
|
) { |
||||||
|
Icon( |
||||||
|
modifier = Modifier.size(24.dp.applyScaleUp()), |
||||||
|
resourceId = if (isPressed) { |
||||||
|
CommonDrawables.ic_compound_mic_on_solid |
||||||
|
} else { |
||||||
|
CommonDrawables.ic_compound_mic_on_outline |
||||||
|
}, |
||||||
|
contentDescription = stringResource(CommonStrings.a11y_voice_message_record), |
||||||
|
tint = ElementTheme.colors.iconSecondary, |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@PreviewsDayNight |
||||||
|
@Composable |
||||||
|
internal fun RecordButtonPreview() = ElementPreview { |
||||||
|
Row { |
||||||
|
RecordButtonView(isPressed = false) |
||||||
|
RecordButtonView(isPressed = true) |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -0,0 +1,74 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.libraries.textcomposer.components |
||||||
|
|
||||||
|
import androidx.compose.foundation.background |
||||||
|
import androidx.compose.foundation.layout.Box |
||||||
|
import androidx.compose.foundation.layout.Row |
||||||
|
import androidx.compose.foundation.layout.Spacer |
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth |
||||||
|
import androidx.compose.foundation.layout.heightIn |
||||||
|
import androidx.compose.foundation.layout.padding |
||||||
|
import androidx.compose.foundation.layout.size |
||||||
|
import androidx.compose.foundation.shape.CircleShape |
||||||
|
import androidx.compose.material3.MaterialTheme |
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.ui.Alignment |
||||||
|
import androidx.compose.ui.Modifier |
||||||
|
import androidx.compose.ui.unit.dp |
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight |
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text |
||||||
|
import io.element.android.libraries.theme.ElementTheme |
||||||
|
|
||||||
|
@Composable |
||||||
|
internal fun RecordingProgress( |
||||||
|
modifier: Modifier = Modifier, |
||||||
|
) { |
||||||
|
Row( |
||||||
|
modifier = modifier |
||||||
|
.fillMaxWidth() |
||||||
|
.background( |
||||||
|
color = ElementTheme.colors.bgSubtleSecondary, |
||||||
|
shape = MaterialTheme.shapes.medium, |
||||||
|
) |
||||||
|
.padding(start = 12.dp, end = 20.dp, top = 8.dp, bottom = 8.dp) |
||||||
|
.heightIn(26.dp) |
||||||
|
|
||||||
|
, |
||||||
|
verticalAlignment = Alignment.CenterVertically, |
||||||
|
) { |
||||||
|
Box( |
||||||
|
modifier = Modifier |
||||||
|
.size(8.dp) |
||||||
|
.background(color = ElementTheme.colors.textCriticalPrimary, shape = CircleShape) |
||||||
|
) |
||||||
|
Spacer(Modifier.size(8.dp)) |
||||||
|
|
||||||
|
// TODO Replace with timer UI |
||||||
|
Text( |
||||||
|
text = "Recording...", // Not localized because it is a placeholder |
||||||
|
color = ElementTheme.colors.textSecondary, |
||||||
|
style = ElementTheme.typography.fontBodySmMedium |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@PreviewsDayNight |
||||||
|
@Composable |
||||||
|
internal fun RecordingProgressPreview() { |
||||||
|
RecordingProgress() |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.libraries.textcomposer.model |
||||||
|
|
||||||
|
sealed class PressEvent { |
||||||
|
data object PressStart: PressEvent() |
||||||
|
data object Tapped: PressEvent() |
||||||
|
data object LongPressEnd: PressEvent() |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.libraries.textcomposer.model |
||||||
|
|
||||||
|
sealed class VoiceMessageState { |
||||||
|
data object Idle: VoiceMessageState() |
||||||
|
data object Recording: VoiceMessageState() |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.libraries.textcomposer.utils |
||||||
|
|
||||||
|
/** |
||||||
|
* State of a press gesture. |
||||||
|
*/ |
||||||
|
internal sealed class PressState { |
||||||
|
data class Idle( |
||||||
|
val lastPress: Pressing? |
||||||
|
) : PressState() |
||||||
|
|
||||||
|
sealed class Pressing : PressState() |
||||||
|
data object Tapping : Pressing() |
||||||
|
data object LongPressing : Pressing() |
||||||
|
} |
||||||
|
|
@ -0,0 +1,47 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.libraries.textcomposer.utils |
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.runtime.LaunchedEffect |
||||||
|
|
||||||
|
/** |
||||||
|
* React to [PressState] changes. |
||||||
|
*/ |
||||||
|
@Composable |
||||||
|
internal fun PressStateEffects( |
||||||
|
pressState: PressState, |
||||||
|
onPressStart: () -> Unit = {}, |
||||||
|
onLongPressStart: () -> Unit = {}, |
||||||
|
onTap: () -> Unit = {}, |
||||||
|
onLongPressEnd: () -> Unit = {}, |
||||||
|
) { |
||||||
|
LaunchedEffect(pressState) { |
||||||
|
when (pressState) { |
||||||
|
is PressState.Idle -> |
||||||
|
when (pressState.lastPress) { |
||||||
|
PressState.Tapping -> onTap() |
||||||
|
PressState.LongPressing -> onLongPressEnd() |
||||||
|
null -> {} // Do nothing |
||||||
|
} |
||||||
|
is PressState.LongPressing -> onLongPressStart() |
||||||
|
PressState.Tapping -> onPressStart() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
@ -0,0 +1,101 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.libraries.textcomposer.utils |
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.runtime.State |
||||||
|
import androidx.compose.runtime.getValue |
||||||
|
import androidx.compose.runtime.mutableStateOf |
||||||
|
import androidx.compose.runtime.remember |
||||||
|
import androidx.compose.runtime.setValue |
||||||
|
import androidx.compose.ui.platform.LocalViewConfiguration |
||||||
|
import kotlinx.coroutines.Job |
||||||
|
import kotlinx.coroutines.coroutineScope |
||||||
|
import kotlinx.coroutines.delay |
||||||
|
import kotlinx.coroutines.isActive |
||||||
|
import kotlinx.coroutines.launch |
||||||
|
import kotlinx.coroutines.yield |
||||||
|
import timber.log.Timber |
||||||
|
|
||||||
|
@Composable |
||||||
|
internal fun rememberPressState( |
||||||
|
longPressTimeoutMillis: Long = LocalViewConfiguration.current.longPressTimeoutMillis, |
||||||
|
): PressStateHolder { |
||||||
|
return remember(longPressTimeoutMillis) { |
||||||
|
PressStateHolder(longPressTimeoutMillis = longPressTimeoutMillis) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* State machine that keeps track of the pressed state. |
||||||
|
* |
||||||
|
* When a press is started, the state will transition through: |
||||||
|
* [PressState.Idle] -> [PressState.Tapping] -> ... |
||||||
|
* |
||||||
|
* If a press is held for a longer time, the state will continue through: |
||||||
|
* ... -> [PressState.LongPressing] -> ... |
||||||
|
* |
||||||
|
* When the press is released the states will then transition back to idle. |
||||||
|
* ... -> [PressState.Idle] |
||||||
|
* |
||||||
|
* Whether a press should be considered a tap or a long press can be determined by |
||||||
|
* looking at the last press when in the idle state. |
||||||
|
* |
||||||
|
* @see [PressStateEffects] |
||||||
|
* @see [rememberPressState] |
||||||
|
*/ |
||||||
|
internal class PressStateHolder( |
||||||
|
private val longPressTimeoutMillis: Long, |
||||||
|
) : State<PressState> { |
||||||
|
private var state: PressState by mutableStateOf(PressState.Idle(lastPress = null)) |
||||||
|
|
||||||
|
override val value: PressState |
||||||
|
get() = state |
||||||
|
|
||||||
|
private var longPressTimer: Job? = null |
||||||
|
|
||||||
|
suspend fun press() = coroutineScope { |
||||||
|
when (state) { |
||||||
|
is PressState.Idle -> { |
||||||
|
state = PressState.Tapping |
||||||
|
} |
||||||
|
is PressState.Pressing -> |
||||||
|
Timber.e("Pointer pressed but it has not been released") |
||||||
|
} |
||||||
|
|
||||||
|
longPressTimer = launch { |
||||||
|
delay(longPressTimeoutMillis) |
||||||
|
yield() |
||||||
|
|
||||||
|
if (isActive && state == PressState.Tapping) { |
||||||
|
state = PressState.LongPressing |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun release() { |
||||||
|
longPressTimer?.cancel() |
||||||
|
longPressTimer = null |
||||||
|
when (val lastState = state) { |
||||||
|
is PressState.Pressing -> |
||||||
|
state = PressState.Idle(lastPress = lastState) |
||||||
|
is PressState.Idle -> |
||||||
|
Timber.e("Pointer pressed but it has not been released") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -0,0 +1,111 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.libraries.textcomposer.utils |
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat |
||||||
|
import io.element.android.libraries.textcomposer.utils.PressState.Idle |
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi |
||||||
|
import kotlinx.coroutines.async |
||||||
|
import kotlinx.coroutines.test.advanceTimeBy |
||||||
|
import kotlinx.coroutines.test.runTest |
||||||
|
import org.junit.Test |
||||||
|
import kotlin.time.Duration.Companion.milliseconds |
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class) class PressStateHolderTest { |
||||||
|
companion object { |
||||||
|
const val LONG_PRESS_TIMEOUT_MILLIS = 1L |
||||||
|
} |
||||||
|
@Test |
||||||
|
fun `it starts in idle state`() = runTest { |
||||||
|
val stateHolder = createStateHolder() |
||||||
|
assertThat(stateHolder.value).isEqualTo(Idle(lastPress = null)) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `when press, it moves to tapping state`() = runTest { |
||||||
|
val stateHolder = createStateHolder() |
||||||
|
val press = async { stateHolder.press() } |
||||||
|
advanceTimeBy(1.milliseconds) |
||||||
|
assertThat(stateHolder.value).isEqualTo(PressState.Tapping) |
||||||
|
press.await() |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `when release after short delay, it moves through tap states`() = runTest { |
||||||
|
val stateHolder = createStateHolder() |
||||||
|
val press = async { stateHolder.press() } |
||||||
|
advanceTimeBy(1.milliseconds) |
||||||
|
assertThat(stateHolder.value).isEqualTo(PressState.Tapping) |
||||||
|
stateHolder.release() |
||||||
|
advanceTimeBy(1.milliseconds) // wait for the long press timeout which should not be triggered |
||||||
|
assertThat(stateHolder.value).isEqualTo(Idle(lastPress = PressState.Tapping)) |
||||||
|
press.await() |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `when hold, it moves through long press states`() = runTest { |
||||||
|
val stateHolder = createStateHolder() |
||||||
|
val press = async { stateHolder.press() } |
||||||
|
advanceTimeBy(1.milliseconds) |
||||||
|
assertThat(stateHolder.value).isEqualTo(PressState.Tapping) |
||||||
|
advanceTimeBy(1.milliseconds) |
||||||
|
assertThat(stateHolder.value).isEqualTo(PressState.LongPressing) |
||||||
|
stateHolder.release() |
||||||
|
assertThat(stateHolder.value).isEqualTo(Idle(lastPress = PressState.LongPressing)) |
||||||
|
press.await() |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `when release and repress, it doesn't enter long press states`() = runTest { |
||||||
|
val stateHolder = createStateHolder() |
||||||
|
val press1 = async { stateHolder.press() } |
||||||
|
advanceTimeBy(1.milliseconds) |
||||||
|
assertThat(stateHolder.value).isEqualTo(PressState.Tapping) |
||||||
|
stateHolder.release() |
||||||
|
val press2 = async { stateHolder.press() } |
||||||
|
advanceTimeBy(1.milliseconds) |
||||||
|
assertThat(stateHolder.value).isEqualTo(PressState.Tapping) |
||||||
|
press1.await() |
||||||
|
press2.await() |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `when press twice without releasing, it doesn't throw an error`() = runTest { |
||||||
|
val stateHolder = createStateHolder() |
||||||
|
stateHolder.press() |
||||||
|
stateHolder.press() |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `when release without first pressing, it doesn't throw an error`() = runTest { |
||||||
|
val stateHolder = createStateHolder() |
||||||
|
stateHolder.release() |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `when release twice without pressing, it doesn't throw an error `() = runTest { |
||||||
|
val stateHolder = createStateHolder() |
||||||
|
stateHolder.press() |
||||||
|
stateHolder.release() |
||||||
|
stateHolder.release() |
||||||
|
} |
||||||
|
|
||||||
|
private fun createStateHolder() = |
||||||
|
PressStateHolder( |
||||||
|
LONG_PRESS_TIMEOUT_MILLIS, |
||||||
|
) |
||||||
|
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-25_25_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-25_25_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-25_25_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-25_25_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-25_25_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-25_25_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-25_26_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-25_26_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-25_26_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-25_26_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-25_26_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-25_26_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-D-26_26_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-D-27_27_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-D-26_26_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-D-27_27_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-N-26_27_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-N-27_28_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-N-26_27_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-N-27_28_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-27_27_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-27_27_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-27_27_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-27_27_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-27_27_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-27_27_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-27_28_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-27_28_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-27_28_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-27_28_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-27_28_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-27_28_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-28_28_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-28_28_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-28_28_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-28_28_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-28_28_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-28_28_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-28_29_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-28_29_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-28_29_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-28_29_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-28_29_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-28_29_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-D-29_29_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-D-30_30_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-D-29_29_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-D-30_30_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-N-29_30_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-N-30_31_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-N-29_30_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-N-30_31_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-30_30_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-31_31_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-30_30_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-31_31_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-30_30_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-31_31_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-30_30_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-31_31_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-30_31_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-31_32_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-30_31_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-31_32_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-30_31_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-31_32_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-30_31_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-31_32_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-32_32_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-33_33_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-32_32_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-33_33_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-32_32_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-33_33_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-32_32_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-33_33_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-32_33_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-33_34_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-32_33_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-33_34_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-32_33_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-33_34_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-32_33_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-33_34_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-31_31_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-32_32_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-31_31_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-32_32_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-31_31_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-32_32_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-31_31_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-32_32_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-31_32_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-32_33_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-31_32_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-32_33_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-31_32_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-32_33_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-31_32_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-32_33_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-33_33_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-34_34_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-33_33_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-34_34_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-33_34_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-34_35_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-33_34_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-34_35_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-34_34_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-35_35_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-34_34_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-35_35_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-34_35_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-35_36_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-34_35_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-35_36_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_3,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_3,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_3,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_3,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_4,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_4,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_4,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_4,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_5,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_5,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_5,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_5,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_0,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_1,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_2,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_2,NEXUS_5,1.0,en].png
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue