Merge branch 'develop' into feature/fix_sync_after_call_invite

This commit is contained in:
Benoit Marty 2020-11-02 14:45:05 +01:00 committed by GitHub
commit 51ae2ce1e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 179 additions and 69 deletions

View File

@ -1,4 +1,4 @@
A full developer contributors list can be found [here](https://github.com/vector-im/element-android/graphs/contributors).
A full developer contributors list can be found [here](https://github.com/vector-im/element-android/graphs/contributors).
# Core team:
@ -33,3 +33,8 @@ First of all, we thank all contributors who use Element and report problems on t
We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element.
Feel free to add your name below, when you contribute to the project!
Name | Matrix ID | GitHub
--------|---------------------|--------------------------------------
gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower)

View File

@ -14,10 +14,16 @@ Improvements 🙌:
- Room profile: BigImageViewerActivity now only display the image. Use the room setting to change or delete the room Avatar
- Better visibility of text reactions in dark theme (#1118)
- Room member profile: Add action to create (or open) a DM (#2310)
- Prepare changelog for F-Droid (#2296)
- Add graphic resources for F-Droid (#812, #2220)
- Highlight text in the body of the displayed result (#2200)
- Considerably faster QR-code bitmap generation (#2331)
Bugfix 🐛:
- Fixed ringtone handling (#2100 & #2246)
- Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252)
- Incoming call continues to ring if call is answered on another device (#1921)
- Search Result | scroll jumps after pagination (#2238)
Translations 🗣:
-

View File

@ -0,0 +1 @@
// TODO

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

View File

@ -167,11 +167,14 @@ internal class DefaultTimeline(
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
?: throw IllegalStateException("Can't open a timeline without a room")
sendingEvents = roomEntity.sendingTimelineEvents.where().filterEventsWithSettings().findAll()
// We don't want to filter here because some sending events that are not displayed
// are still used for ui echo (relation like reaction)
sendingEvents = roomEntity.sendingTimelineEvents.where()/*.filterEventsWithSettings()*/.findAll()
sendingEvents.addChangeListener { events ->
uiEchoManager.sentEventsUpdated(events)
postSnapshot()
}
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
filteredEvents = nonFilteredEvents.where()
.filterEventsWithSettings()
@ -410,13 +413,16 @@ internal class DefaultTimeline(
val builtSendingEvents = ArrayList<TimelineEvent>()
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
builtSendingEvents.addAll(uiEchoManager.getInMemorySendingEvents().filterEventsWithSettings())
sendingEvents.forEach { timelineEventEntity ->
if (builtSendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) {
val element = timelineEventMapper.map(timelineEventEntity)
uiEchoManager.updateSentStateWithUiEcho(element)
builtSendingEvents.add(element)
}
}
sendingEvents
.map { timelineEventMapper.map(it) }
// Filter out sending event that are not displayable!
.filterEventsWithSettings()
.forEach { timelineEvent ->
if (builtSendingEvents.find { it.eventId == timelineEvent.eventId } == null) {
uiEchoManager.updateSentStateWithUiEcho(timelineEvent)
builtSendingEvents.add(timelineEvent)
}
}
}
return builtSendingEvents
}
@ -782,8 +788,8 @@ internal class DefaultTimeline(
val filterType = !settings.filters.filterTypes || settings.filters.allowedTypes.contains(it.root.type)
if (!filterType) return@filter false
val filterEdits = if (settings.filters.filterEdits && it.root.type == EventType.MESSAGE) {
val messageContent = it.root.content.toModel<MessageContent>()
val filterEdits = if (settings.filters.filterEdits && it.root.getClearType() == EventType.MESSAGE) {
val messageContent = it.root.getClearContent().toModel<MessageContent>()
messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE
} else {
true
@ -813,7 +819,7 @@ internal class DefaultTimeline(
private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
fun getInMemorySendingEvents(): List<TimelineEvent> {
return inMemorySendingEvents
return inMemorySendingEvents.toList()
}
/**
@ -831,8 +837,13 @@ internal class DefaultTimeline(
inMemorySendingStates.keys.removeAll { key ->
events.find { it.eventId == key } == null
}
inMemoryReactions.keys.removeAll { key ->
events.find { it.eventId == key } == null
inMemoryReactions.forEach { (_, uiEchoData) ->
uiEchoData.removeAll { data ->
// I remove the uiEcho, when the related event is not anymore in the sending list
// (means that it is synced)!
events.find { it.eventId == data.localEchoId } == null
}
}
}
@ -896,6 +907,7 @@ internal class DefaultTimeline(
relatedEventID
)
val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList()
contents.forEach { uiEchoReaction ->
val existing = updateReactions.firstOrNull { it.key == uiEchoReaction.reaction }
if (existing == null) {

View File

@ -34,12 +34,15 @@ fun String.toBitMatrix(size: Int): BitMatrix {
fun BitMatrix.toBitmap(@ColorInt backgroundColor: Int = Color.WHITE,
@ColorInt foregroundColor: Int = Color.BLACK): Bitmap {
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
for (x in 0 until width) {
for (y in 0 until height) {
bmp.setPixel(x, y, if (get(x, y)) foregroundColor else backgroundColor)
val colorBuffer = IntArray(width * height)
var rowOffset = 0
for (y in 0 until height) {
for (x in 0 until width) {
val arrayIndex = x + rowOffset
colorBuffer[arrayIndex] = if (get(x, y)) foregroundColor else backgroundColor
}
rowOffset += width
}
return bmp
return Bitmap.createBitmap(colorBuffer, width, height, Bitmap.Config.ARGB_8888)
}

View File

@ -17,6 +17,8 @@
package im.vector.app.core.services
import android.content.Context
import android.media.Ringtone
import android.media.RingtoneManager
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.MediaPlayer
@ -25,7 +27,26 @@ import androidx.core.content.getSystemService
import im.vector.app.R
import timber.log.Timber
class CallRingPlayer(
class CallRingPlayerIncoming(
context: Context
) {
private val applicationContext = context.applicationContext
private var r: Ringtone? = null
fun start() {
val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
r = RingtoneManager.getRingtone(applicationContext, notification)
Timber.v("## VOIP Starting ringing incomming")
r?.play()
}
fun stop() {
r?.stop()
}
}
class CallRingPlayerOutgoing(
context: Context
) {
@ -44,12 +65,12 @@ class CallRingPlayer(
try {
if (player?.isPlaying == false) {
player?.start()
Timber.v("## VOIP Starting ringing")
Timber.v("## VOIP Starting ringing outgoing")
} else {
Timber.v("## VOIP already playing")
}
} catch (failure: Throwable) {
Timber.e(failure, "## VOIP Failed to start ringing")
Timber.e(failure, "## VOIP Failed to start ringing outgoing")
player = null
}
} else {
@ -74,7 +95,7 @@ class CallRingPlayer(
} else {
mediaPlayer.setAudioAttributes(AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build())
}
return mediaPlayer

View File

@ -40,7 +40,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
private lateinit var notificationUtils: NotificationUtils
private lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
private var callRingPlayer: CallRingPlayer? = null
private var callRingPlayerIncoming: CallRingPlayerIncoming? = null
private var callRingPlayerOutgoing: CallRingPlayerOutgoing? = null
private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null
private var bluetoothHeadsetStateReceiver: BluetoothHeadsetReceiver? = null
@ -63,14 +64,16 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
super.onCreate()
notificationUtils = vectorComponent().notificationUtils()
webRtcPeerConnectionManager = vectorComponent().webRtcPeerConnectionManager()
callRingPlayer = CallRingPlayer(applicationContext)
callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext)
callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext)
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this)
bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(this, this)
}
override fun onDestroy() {
super.onDestroy()
callRingPlayer?.stop()
callRingPlayerIncoming?.stop()
callRingPlayerOutgoing?.stop()
wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(this, it) }
wiredHeadsetStateReceiver = null
bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(this, it) }
@ -100,16 +103,17 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
when (intent.action) {
ACTION_INCOMING_RINGING_CALL -> {
mediaSession?.isActive = true
callRingPlayer?.start()
callRingPlayerIncoming?.start()
displayIncomingCallNotification(intent)
}
ACTION_OUTGOING_RINGING_CALL -> {
mediaSession?.isActive = true
callRingPlayer?.start()
callRingPlayerOutgoing?.start()
displayOutgoingRingingCallNotification(intent)
}
ACTION_ONGOING_CALL -> {
callRingPlayer?.stop()
callRingPlayerIncoming?.stop()
callRingPlayerOutgoing?.stop()
displayCallInProgressNotification(intent)
}
ACTION_NO_ACTIVE_CALL -> hideCallNotifications()
@ -117,7 +121,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
// lower notification priority
displayCallInProgressNotification(intent)
// stop ringing
callRingPlayer?.stop()
callRingPlayerIncoming?.stop()
callRingPlayerOutgoing?.stop()
}
ACTION_ONGOING_CALL_BG -> {
// there is an ongoing call but call activity is in background
@ -125,7 +130,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
}
else -> {
// Should not happen
callRingPlayer?.stop()
callRingPlayerIncoming?.stop()
callRingPlayerOutgoing?.stop()
myStopSelf()
}
}

View File

@ -93,6 +93,10 @@ class CallAudioManager(
fun startForCall(mxCall: MxCall) {
Timber.v("## VOIP: AudioManager startForCall ${mxCall.callId}")
}
private fun setupAudioManager(mxCall: MxCall) {
Timber.v("## VOIP: AudioManager setupAudioManager ${mxCall.callId}")
val audioManager = audioManager ?: return
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn
savedIsMicrophoneMute = audioManager.isMicrophoneMute
@ -150,7 +154,7 @@ class CallAudioManager(
fun onCallConnected(mxCall: MxCall) {
Timber.v("##VOIP: AudioManager call answered, adjusting current sound device")
adjustCurrentSoundDevice(mxCall)
setupAudioManager(mxCall)
}
fun getAvailableSoundDevices(): List<SoundDevice> {

View File

@ -52,8 +52,6 @@ class SearchFragment @Inject constructor(
private val fragmentArgs: SearchArgs by args()
private val searchViewModel: SearchViewModel by fragmentViewModel()
private var pendingScrollToPosition: Int? = null
override fun getLayoutResId() = R.layout.fragment_search
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -70,12 +68,6 @@ class SearchFragment @Inject constructor(
searchResultRecycler.configureWith(controller, showDivider = false)
(searchResultRecycler.layoutManager as? LinearLayoutManager)?.stackFromEnd = true
controller.listener = this
controller.addModelBuildListener {
pendingScrollToPosition?.let {
searchResultRecycler.smoothScrollToPosition(it)
}
}
}
override fun onDestroy() {
@ -100,10 +92,8 @@ class SearchFragment @Inject constructor(
}
}
} else {
pendingScrollToPosition = (state.lastBatchSize - 1).coerceAtLeast(0)
stateView.state = StateView.State.Content
controller.setData(state)
stateView.state = StateView.State.Content
}
}

View File

@ -16,16 +16,24 @@
package im.vector.app.features.home.room.detail.search
import android.graphics.Typeface
import android.text.Spannable
import android.text.SpannableString
import android.text.style.StyleSpan
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.epoxy.VisibilityState
import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.ui.list.genericItemHeader
import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.GenericItemHeader_
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.search.EventAndSender
import org.matrix.android.sdk.api.util.toMatrixItem
import java.util.Calendar
import javax.inject.Inject
@ -33,6 +41,7 @@ import javax.inject.Inject
class SearchResultController @Inject constructor(
private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val dateFormatter: VectorDateFormatter
) : TypedEpoxyController<SearchViewState>() {
@ -52,6 +61,8 @@ class SearchResultController @Inject constructor(
override fun buildModels(data: SearchViewState?) {
data ?: return
val searchItems = buildSearchResultItems(data)
if (data.hasMoreResult) {
loadingItem {
// Always use a different id, because we can be notified several times of visibility state changed
@ -62,35 +73,85 @@ class SearchResultController @Inject constructor(
}
}
}
} else {
if (searchItems.isEmpty()) {
// All returned results by the server has been filtered out and there is no more result
noResultItem {
id("noResult")
text(stringProvider.getString(R.string.no_result_placeholder))
}
} else {
noResultItem {
id("noMoreResult")
text(stringProvider.getString(R.string.no_more_results))
}
}
}
buildSearchResultItems(data.searchResult)
searchItems.forEach { add(it) }
}
private fun buildSearchResultItems(events: List<EventAndSender>) {
/**
* @return the list of EpoxyModel (date items and search result items), or an empty list if all items have been filtered out
*/
private fun buildSearchResultItems(data: SearchViewState): List<EpoxyModel<*>> {
var lastDate: Calendar? = null
val result = mutableListOf<EpoxyModel<*>>()
data.searchResult.forEach { eventAndSender ->
val event = eventAndSender.event
@Suppress("UNCHECKED_CAST")
// Take new content first
val text = ((event.content?.get("m.new_content") as? Content) ?: event.content)?.get("body") as? String ?: return@forEach
val spannable = setHighLightedText(text, data.highlights) ?: return@forEach
events.forEach { eventAndSender ->
val eventDate = Calendar.getInstance().apply {
timeInMillis = eventAndSender.event.originServerTs ?: System.currentTimeMillis()
}
if (lastDate?.get(Calendar.DAY_OF_YEAR) != eventDate.get(Calendar.DAY_OF_YEAR)) {
genericItemHeader {
id(eventDate.hashCode())
text(dateFormatter.format(eventDate.timeInMillis, DateFormatKind.EDIT_HISTORY_HEADER))
}
GenericItemHeader_()
.id(eventDate.hashCode())
.text(dateFormatter.format(eventDate.timeInMillis, DateFormatKind.EDIT_HISTORY_HEADER))
.let { result.add(it) }
}
lastDate = eventDate
searchResultItem {
id(eventAndSender.event.eventId)
avatarRenderer(avatarRenderer)
dateFormatter(dateFormatter)
event(eventAndSender.event)
sender(eventAndSender.sender
?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem())
listener { listener?.onItemClicked(eventAndSender.event) }
SearchResultItem_()
.id(eventAndSender.event.eventId)
.avatarRenderer(avatarRenderer)
.formattedDate(dateFormatter.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE))
.spannable(spannable)
.sender(eventAndSender.sender
?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem())
.listener { listener?.onItemClicked(eventAndSender.event) }
.let { result.add(it) }
}
return result
}
/**
* Highlight the text. If the text is not found, return null to ignore this result
* See https://github.com/matrix-org/synapse/issues/8686
*/
private fun setHighLightedText(text: String, highlights: List<String>): Spannable? {
val wordToSpan: Spannable = SpannableString(text)
var found = false
highlights.forEach { highlight ->
var searchFromIndex = 0
while (searchFromIndex < text.length) {
val indexOfHighlight = text.indexOf(highlight, searchFromIndex, ignoreCase = true)
searchFromIndex = if (indexOfHighlight == -1) {
Integer.MAX_VALUE
} else {
// bold
found = true
wordToSpan.setSpan(StyleSpan(Typeface.BOLD), indexOfHighlight, indexOfHighlight + highlight.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
indexOfHighlight + 1
}
}
}
return wordToSpan.takeIf { found }
}
}

View File

@ -21,23 +21,20 @@ import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_search_result)
abstract class SearchResultItem : VectorEpoxyModel<SearchResultItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute var dateFormatter: VectorDateFormatter? = null
@EpoxyAttribute lateinit var event: Event
@EpoxyAttribute var formattedDate: String? = null
@EpoxyAttribute lateinit var spannable: CharSequence
@EpoxyAttribute var sender: MatrixItem? = null
@EpoxyAttribute var listener: ClickListener? = null
@ -47,9 +44,8 @@ abstract class SearchResultItem : VectorEpoxyModel<SearchResultItem.Holder>() {
holder.view.onClick(listener)
sender?.let { avatarRenderer.render(it, holder.avatarImageView) }
holder.memberNameView.setTextOrHide(sender?.getBestName())
holder.timeView.text = dateFormatter?.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
// TODO Improve that (use formattedBody, etc.)
holder.contentView.text = event.content?.get("body") as? String
holder.timeView.text = formattedDate
holder.contentView.text = spannable
}
class Holder : VectorEpoxyHolder() {

View File

@ -145,6 +145,7 @@ class SearchViewModel @AssistedInject constructor(
setState {
copy(
searchResult = accumulatedResult,
highlights = searchResult.highlights.orEmpty(),
hasMoreResult = !nextBatch.isNullOrEmpty(),
lastBatchSize = searchResult.results.orEmpty().size,
asyncSearchRequest = Success(Unit)

View File

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.search.EventAndSender
data class SearchViewState(
// Accumulated search result
val searchResult: List<EventAndSender> = emptyList(),
val highlights: List<String> = emptyList(),
val hasMoreResult: Boolean = false,
// Last batch size, will help RecyclerView to position itself
val lastBatchSize: Int = 0,

View File

@ -8,8 +8,10 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/searchResultRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:overScrollMode="always"
tools:itemCount="2"
tools:listitem="@layout/item_search_result" />
</im.vector.app.core.platform.StateView>

View File

@ -173,6 +173,7 @@
<string name="no_conversation_placeholder">No conversations</string>
<string name="no_contact_access_placeholder">You didnt allow Element to access your local contacts</string>
<string name="no_result_placeholder">No results</string>
<string name="no_more_results">No more results</string>
<string name="people_no_identity_server">No identity server configured.</string>
<!-- Rooms fragment -->