Merge pull request #313 from vector-im/feature/notif_optim

Improve notification drawer manager: Dagger, throttle, and icon for API 9
This commit is contained in:
Benoit Marty 2019-07-08 17:44:10 +02:00 committed by GitHub
commit 7ce476f858
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 63 additions and 163 deletions

View File

@ -19,7 +19,7 @@ package im.vector.riotx.core.extensions
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import im.vector.riotx.core.utils.Debouncer import im.vector.riotx.core.utils.FirstThrottler
import im.vector.riotx.core.utils.EventObserver import im.vector.riotx.core.utils.EventObserver
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent


@ -35,11 +35,11 @@ inline fun <T> LiveData<LiveEvent<T>>.observeEvent(owner: LifecycleOwner, crossi
this.observe(owner, EventObserver { it.run(observer) }) this.observe(owner, EventObserver { it.run(observer) })
} }


inline fun <T> LiveData<LiveEvent<T>>.observeEventDebounced(owner: LifecycleOwner, minimumInterval: Long, crossinline observer: (T) -> Unit) { inline fun <T> LiveData<LiveEvent<T>>.observeEventFirstThrottle(owner: LifecycleOwner, minimumInterval: Long, crossinline observer: (T) -> Unit) {
val debouncer = Debouncer(minimumInterval) val firstThrottler = FirstThrottler(minimumInterval)


this.observe(owner, EventObserver { this.observe(owner, EventObserver {
if (debouncer.canHandle()) { if (firstThrottler.canHandle()) {
it.run(observer) it.run(observer)
} }
}) })

View File

@ -17,9 +17,10 @@ package im.vector.riotx.core.utils




/** /**
* Simple Debouncer * Simple ThrottleFirst
* See https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleFirst.png
*/ */
class Debouncer(private val minimumInterval: Long = 800) { class FirstThrottler(private val minimumInterval: Long = 800) {
private var lastDate = 0L private var lastDate = 0L


fun canHandle(): Boolean { fun canHandle(): Boolean {

View File

@ -33,7 +33,7 @@ import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.observeEventDebounced import im.vector.riotx.core.extensions.observeEventFirstThrottle
import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.OnBackPressed
import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
@ -81,7 +81,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
setupCreateRoomButton() setupCreateRoomButton()
setupRecyclerView() setupRecyclerView()
roomListViewModel.subscribe { renderState(it) } roomListViewModel.subscribe { renderState(it) }
roomListViewModel.openRoomLiveData.observeEventDebounced(this, 800L) { roomListViewModel.openRoomLiveData.observeEventFirstThrottle(this, 800L) {
navigator.openRoom(requireActivity(), it) navigator.openRoom(requireActivity(), it)
} }



View File

@ -18,74 +18,39 @@ package im.vector.riotx.features.notifications


import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Handler
import android.os.HandlerThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.DecodeFormat
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton


/** @Singleton
* FIXME It works, but it does not refresh the notification, when it's already displayed class BitmapLoader @Inject constructor(val context: Context) {
*/
class BitmapLoader(val context: Context,
val listener: BitmapLoaderListener) {


/** /**
* Avatar Url -> Icon * Avatar Url -> Bitmap
*/ */
private val cache = HashMap<String, Bitmap>() private val cache = HashMap<String, Bitmap?>()

// URLs to load
private val toLoad = HashSet<String>()

// Black list of URLs (broken URL, etc.)
private val blacklist = HashSet<String>()

private var uiHandler = Handler()

private val handlerThread: HandlerThread = HandlerThread("BitmapLoader", Thread.MIN_PRIORITY)
private var backgroundHandler: Handler

init {
handlerThread.start()
backgroundHandler = Handler(handlerThread.looper)
}


/** /**
* Get icon of a room. * Get icon of a room.
* If already in cache, use it, else load it and call BitmapLoaderListener.onBitmapsLoaded() when ready * If already in cache, use it, else load it and call BitmapLoaderListener.onBitmapsLoaded() when ready
*/ */
@WorkerThread
fun getRoomBitmap(path: String?): Bitmap? { fun getRoomBitmap(path: String?): Bitmap? {
if (path == null) { if (path == null) {
return null return null
} }


synchronized(cache) { return cache.getOrPut(path) {
if (cache[path] != null) { loadRoomBitmap(path)
return cache[path]
}

// Add to the queue, if not blacklisted
if (!blacklist.contains(path)) {
if (toLoad.contains(path)) {
// Wait
} else {
toLoad.add(path)

backgroundHandler.post {
loadRoomBitmap(path)
}
}
}
} }

return null
} }


@WorkerThread @WorkerThread
private fun loadRoomBitmap(path: String) { private fun loadRoomBitmap(path: String): Bitmap? {
val bitmap = path.let { return path.let {
try { try {
Glide.with(context) Glide.with(context)
.asBitmap() .asBitmap()
@ -98,27 +63,5 @@ class BitmapLoader(val context: Context,
null null
} }
} }

synchronized(cache) {
if (bitmap == null) {
// Add to the blacklist
blacklist.add(path)
} else {
cache[path] = bitmap
}

toLoad.remove(path)

if (toLoad.isEmpty()) {
uiHandler.post {
listener.onBitmapsLoaded()
}
}
}
} }

}

interface BitmapLoaderListener {
fun onBitmapsLoaded()
}
}

View File

@ -17,76 +17,43 @@
package im.vector.riotx.features.notifications package im.vector.riotx.features.notifications


import android.content.Context import android.content.Context
import android.os.Handler import android.os.Build
import android.os.HandlerThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton


/** @Singleton
* FIXME It works, but it does not refresh the notification, when it's already displayed class IconLoader @Inject constructor(val context: Context) {
*/
class IconLoader(val context: Context,
val listener: IconLoaderListener) {


/** /**
* Avatar Url -> Icon * Avatar Url -> IconCompat
*/ */
private val cache = HashMap<String, IconCompat>() private val cache = HashMap<String, IconCompat?>()

// URLs to load
private val toLoad = HashSet<String>()

// Black list of URLs (broken URL, etc.)
private val blacklist = HashSet<String>()

private var uiHandler = Handler()

private val handlerThread: HandlerThread = HandlerThread("IconLoader", Thread.MIN_PRIORITY)
private var backgroundHandler: Handler

init {
handlerThread.start()
backgroundHandler = Handler(handlerThread.looper)
}


/** /**
* Get icon of a user. * Get icon of a user.
* If already in cache, use it, else load it and call IconLoaderListener.onIconsLoaded() when ready * If already in cache, use it, else load it and call IconLoaderListener.onIconsLoaded() when ready
* Before Android P, this does nothing because the icon won't be used
*/ */
@WorkerThread
fun getUserIcon(path: String?): IconCompat? { fun getUserIcon(path: String?): IconCompat? {
if (path == null) { if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return null return null
} }


synchronized(cache) { return cache.getOrPut(path) {
if (cache[path] != null) { loadUserIcon(path)
return cache[path]
}

// Add to the queue, if not blacklisted
if (!blacklist.contains(path)) {
if (toLoad.contains(path)) {
// Wait
} else {
toLoad.add(path)

backgroundHandler.post {
loadUserIcon(path)
}
}
}
} }

return null
} }


@WorkerThread @WorkerThread
private fun loadUserIcon(path: String) { private fun loadUserIcon(path: String): IconCompat? {
val iconCompat = path.let { return path.let {
try { try {
Glide.with(context) Glide.with(context)
.asBitmap() .asBitmap()
@ -102,27 +69,5 @@ class IconLoader(val context: Context,
IconCompat.createWithBitmap(bitmap) IconCompat.createWithBitmap(bitmap)
} }
} }

synchronized(cache) {
if (iconCompat == null) {
// Add to the blacklist
blacklist.add(path)
} else {
cache[path] = iconCompat
}

toLoad.remove(path)

if (toLoad.isEmpty()) {
uiHandler.post {
listener.onIconsLoaded()
}
}
}
} }

}

interface IconLoaderListener {
fun onIconsLoaded()
}
}

View File

@ -18,6 +18,9 @@ package im.vector.riotx.features.notifications
import android.app.Notification import android.app.Notification
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Handler
import android.os.HandlerThread
import androidx.annotation.WorkerThread
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.Person import androidx.core.app.Person
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
@ -42,8 +45,18 @@ import javax.inject.Singleton
@Singleton @Singleton
class NotificationDrawerManager @Inject constructor(private val context: Context, class NotificationDrawerManager @Inject constructor(private val context: Context,
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val iconLoader: IconLoader,
private val bitmapLoader: BitmapLoader,
private val outdatedDetector: OutdatedEventDetector?) { private val outdatedDetector: OutdatedEventDetector?) {


private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY)
private var backgroundHandler: Handler

init {
handlerThread.start()
backgroundHandler = Handler(handlerThread.looper)
}

//The first time the notification drawer is refreshed, we force re-render of all notifications //The first time the notification drawer is refreshed, we force re-render of all notifications
private var firstTime = true private var firstTime = true


@ -53,22 +66,6 @@ class NotificationDrawerManager @Inject constructor(private val context: Context


private var currentRoomId: String? = null private var currentRoomId: String? = null


private var iconLoader = IconLoader(context,
object : IconLoader.IconLoaderListener {
override fun onIconsLoaded() {
// Force refresh
refreshNotificationDrawer()
}
})

private var bitmapLoader = BitmapLoader(context,
object : BitmapLoader.BitmapLoaderListener {
override fun onBitmapsLoaded() {
// Force refresh
refreshNotificationDrawer()
}
})

/** /**
Should be called as soon as a new event is ready to be displayed. Should be called as soon as a new event is ready to be displayed.
The notification corresponding to this event will not be displayed until The notification corresponding to this event will not be displayed until
@ -171,6 +168,20 @@ class NotificationDrawerManager @Inject constructor(private val context: Context




fun refreshNotificationDrawer() { fun refreshNotificationDrawer() {
// Implement last throttler
Timber.w("refreshNotificationDrawer()")
backgroundHandler.removeCallbacksAndMessages(null)
backgroundHandler.postDelayed(
{
refreshNotificationDrawerBg()
}
, 200)
}

@WorkerThread
private fun refreshNotificationDrawerBg() {
Timber.w("refreshNotificationDrawerBg()")

val session = activeSessionHolder.getActiveSession() val session = activeSessionHolder.getActiveSession()
val user = session.getUser(session.sessionParams.credentials.userId) val user = session.getUser(session.sessionParams.credentials.userId)
val myUserDisplayName = user?.displayName ?: "" val myUserDisplayName = user?.displayName ?: ""