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.LiveData
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.LiveEvent

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

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

this.observe(owner, EventObserver {
if (debouncer.canHandle()) {
if (firstThrottler.canHandle()) {
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

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.error.ErrorFormatter
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.StateView
import im.vector.riotx.core.platform.VectorBaseFragment
@ -81,7 +81,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
setupCreateRoomButton()
setupRecyclerView()
roomListViewModel.subscribe { renderState(it) }
roomListViewModel.openRoomLiveData.observeEventDebounced(this, 800L) {
roomListViewModel.openRoomLiveData.observeEventFirstThrottle(this, 800L) {
navigator.openRoom(requireActivity(), it)
}


View File

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

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

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

/**
* Avatar Url -> Icon
* Avatar Url -> 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)
}
private val cache = HashMap<String, Bitmap?>()

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

synchronized(cache) {
if (cache[path] != null) {
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 cache.getOrPut(path) {
loadRoomBitmap(path)
}

return null
}

@WorkerThread
private fun loadRoomBitmap(path: String) {
val bitmap = path.let {
private fun loadRoomBitmap(path: String): Bitmap? {
return path.let {
try {
Glide.with(context)
.asBitmap()
@ -98,27 +63,5 @@ class BitmapLoader(val context: Context,
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

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

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

/**
* Avatar Url -> Icon
* Avatar Url -> 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)
}
private val cache = HashMap<String, IconCompat?>()

/**
* Get icon of a user.
* 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? {
if (path == null) {
if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return null
}

synchronized(cache) {
if (cache[path] != null) {
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 cache.getOrPut(path) {
loadUserIcon(path)
}

return null
}

@WorkerThread
private fun loadUserIcon(path: String) {
val iconCompat = path.let {
private fun loadUserIcon(path: String): IconCompat? {
return path.let {
try {
Glide.with(context)
.asBitmap()
@ -102,27 +69,5 @@ class IconLoader(val context: Context,
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.content.Context
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.Person
import im.vector.matrix.android.api.session.content.ContentUrlResolver
@ -42,8 +45,18 @@ import javax.inject.Singleton
@Singleton
class NotificationDrawerManager @Inject constructor(private val context: Context,
private val activeSessionHolder: ActiveSessionHolder,
private val iconLoader: IconLoader,
private val bitmapLoader: BitmapLoader,
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
private var firstTime = true

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

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.
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() {
// 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 user = session.getUser(session.sessionParams.credentials.userId)
val myUserDisplayName = user?.displayName ?: ""