From 917042c48cff740931519b86f63f20e6f74b48fe Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 10 Jun 2020 16:14:19 +0200 Subject: [PATCH] Remove memory leak from Autocompleter --- .../android/api/auth/data/LoginFlowResult.kt | 2 - .../autocomplete/RecyclerViewPresenter.kt | 125 ++++++++++++++++++ .../command/AutocompleteCommandPresenter.kt | 8 +- .../emoji/AutocompleteEmojiPresenter.kt | 8 +- .../group/AutocompleteGroupPresenter.kt | 8 +- .../member/AutocompleteMemberPresenter.kt | 8 +- .../room/AutocompleteRoomPresenter.kt | 8 +- .../UnknownDeviceDetectorSharedViewModel.kt | 4 - .../home/room/detail/AutoCompleter.kt | 22 ++- .../home/room/detail/RoomDetailFragment.kt | 1 + 10 files changed, 168 insertions(+), 26 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/RecyclerViewPresenter.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt index abc1b83c9f..3e824eeaee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt @@ -16,8 +16,6 @@ package im.vector.matrix.android.api.auth.data -import im.vector.matrix.android.internal.auth.data.LoginFlowResponse - // Either a list of supported login types, or an error if the homeserver is outdated sealed class LoginFlowResult { data class Success( diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/RecyclerViewPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/RecyclerViewPresenter.kt new file mode 100644 index 0000000000..371e9a2473 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/RecyclerViewPresenter.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020 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 im.vector.riotx.features.autocomplete + +import android.content.Context +import android.database.DataSetObserver +import android.view.ViewGroup +import androidx.annotation.CallSuper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.otaliastudios.autocomplete.AutocompletePresenter + +abstract class RecyclerViewPresenter(context: Context?) : AutocompletePresenter(context) { + + private var recyclerView: RecyclerView? = null + private var clicks: ClickProvider? = null + private var observer: RecyclerView.AdapterDataObserver? = null + + override fun registerClickProvider(provider: ClickProvider) { + clicks = provider + } + + override fun registerDataSetObserver(observer: DataSetObserver) { + this.observer = Observer(observer) + } + + @CallSuper + override fun getView(): ViewGroup { + val adapter = instantiateAdapter() + observer?.also { + adapter.registerAdapterDataObserver(it) + } + return RecyclerView(context).apply { + this.adapter = adapter + this.layoutManager = instantiateLayoutManager() + this.itemAnimator = null + } + } + + override fun onViewShown() {} + @CallSuper + override fun onViewHidden() { + observer?.also { + recyclerView?.adapter?.unregisterAdapterDataObserver(it) + } + recyclerView = null + observer = null + } + + /** + * Dispatch click event to Autocomplete.Callback. + * Should be called when items are clicked. + * + * @param item the clicked item. + */ + protected fun dispatchClick(item: T) { + if (clicks != null) clicks?.click(item) + } + + /** + * Request that the popup should recompute its dimensions based on a recent change in + * the view being displayed. + * + * This is already managed internally for [RecyclerView] events. + * Only use it for changes in other views that you have added to the popup, + * and only if one of the dimensions for the popup is WRAP_CONTENT . + */ + protected fun dispatchLayoutChange() { + if (observer != null) observer!!.onChanged() + } + + /** + * Provide an adapter for the recycler. + * This should be a fresh instance every time this is called. + * + * @return a new adapter. + */ + protected abstract fun instantiateAdapter(): RecyclerView.Adapter<*> + + /** + * Provides a layout manager for the recycler. + * This should be a fresh instance every time this is called. + * Defaults to a vertical LinearLayoutManager, which is guaranteed to work well. + * + * @return a new layout manager. + */ + protected fun instantiateLayoutManager(): RecyclerView.LayoutManager { + return LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + } + + private class Observer internal constructor(private val root: DataSetObserver) : RecyclerView.AdapterDataObserver() { + override fun onChanged() { + root.onChanged() + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { + root.onChanged() + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { + root.onChanged() + } + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + root.onChanged() + } + + override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { + root.onChanged() + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt index 84ae8db217..caadb3cc91 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt @@ -18,8 +18,8 @@ package im.vector.riotx.features.autocomplete.command import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.otaliastudios.autocomplete.RecyclerViewPresenter import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.autocomplete.RecyclerViewPresenter import im.vector.riotx.features.command.Command import javax.inject.Inject @@ -32,8 +32,6 @@ class AutocompleteCommandPresenter @Inject constructor(context: Context, } override fun instantiateAdapter(): RecyclerView.Adapter<*> { - // Also remove animation - recyclerView?.itemAnimator = null return controller.adapter } @@ -51,4 +49,8 @@ class AutocompleteCommandPresenter @Inject constructor(context: Context, } controller.setData(data) } + + fun clear() { + controller.listener = null + } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt index 731b48af86..2661e4ac2b 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt @@ -18,8 +18,8 @@ package im.vector.riotx.features.autocomplete.emoji import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.otaliastudios.autocomplete.RecyclerViewPresenter import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.autocomplete.RecyclerViewPresenter import im.vector.riotx.features.reactions.data.EmojiDataSource import javax.inject.Inject @@ -32,9 +32,11 @@ class AutocompleteEmojiPresenter @Inject constructor(context: Context, controller.listener = this } + fun clear(){ + controller.listener = null + } + override fun instantiateAdapter(): RecyclerView.Adapter<*> { - // Also remove animation - recyclerView?.itemAnimator = null return controller.adapter } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt index b6f45b477c..171551d3d3 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt @@ -18,12 +18,12 @@ package im.vector.riotx.features.autocomplete.group import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.otaliastudios.autocomplete.RecyclerViewPresenter import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.group.groupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.autocomplete.RecyclerViewPresenter import javax.inject.Inject class AutocompleteGroupPresenter @Inject constructor(context: Context, @@ -35,9 +35,11 @@ class AutocompleteGroupPresenter @Inject constructor(context: Context, controller.listener = this } + fun clear(){ + controller.listener = null + } + override fun instantiateAdapter(): RecyclerView.Adapter<*> { - // Also remove animation - recyclerView?.itemAnimator = null return controller.adapter } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt index 4be19f2e73..70c0b7a9f0 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt @@ -18,7 +18,6 @@ package im.vector.riotx.features.autocomplete.member import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.otaliastudios.autocomplete.RecyclerViewPresenter import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.query.QueryStringValue @@ -27,6 +26,7 @@ import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.autocomplete.RecyclerViewPresenter class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, @Assisted val roomId: String, @@ -40,14 +40,16 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, controller.listener = this } + fun clear(){ + controller.listener = null + } + @AssistedInject.Factory interface Factory { fun create(roomId: String): AutocompleteMemberPresenter } override fun instantiateAdapter(): RecyclerView.Adapter<*> { - // Also remove animation - recyclerView?.itemAnimator = null return controller.adapter } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt index 17787a22ef..c845decdf9 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt @@ -18,12 +18,12 @@ package im.vector.riotx.features.autocomplete.room import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.otaliastudios.autocomplete.RecyclerViewPresenter import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.roomSummaryQueryParams import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.autocomplete.RecyclerViewPresenter import javax.inject.Inject class AutocompleteRoomPresenter @Inject constructor(context: Context, @@ -36,8 +36,6 @@ class AutocompleteRoomPresenter @Inject constructor(context: Context, } override fun instantiateAdapter(): RecyclerView.Adapter<*> { - // Also remove animation - recyclerView?.itemAnimator = null return controller.adapter } @@ -58,4 +56,8 @@ class AutocompleteRoomPresenter @Inject constructor(context: Context, .sortedBy { it.displayName } controller.setData(rooms.toList()) } + + fun clear() { + controller.listener = null + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt index 00700868f1..a61d726ac7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -36,13 +36,10 @@ import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo import im.vector.matrix.rx.rx -import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.platform.EmptyViewEvents import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.features.settings.VectorPreferences -import im.vector.riotx.features.widgets.WidgetViewModel -import im.vector.riotx.features.widgets.WidgetViewState import io.reactivex.Observable import io.reactivex.functions.Function3 import timber.log.Timber @@ -168,5 +165,4 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList) super.onCleared() } - } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 314baab011..273c0ea08e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -34,6 +34,7 @@ import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toRoomAliasMatrixItem import im.vector.riotx.R import im.vector.riotx.core.glide.GlideApp +import im.vector.riotx.core.glide.GlideRequests import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.emoji.AutocompleteEmojiPresenter @@ -56,12 +57,14 @@ class AutoCompleter @AssistedInject constructor( private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter ) { + private lateinit var autocompleteMemberPresenter: AutocompleteMemberPresenter + @AssistedInject.Factory interface Factory { fun create(roomId: String): AutoCompleter } - private lateinit var editText: EditText + private var editText: EditText? = null fun enterSpecialMode() { commandAutocompletePolicy.enabled = false @@ -71,12 +74,11 @@ class AutoCompleter @AssistedInject constructor( commandAutocompletePolicy.enabled = true } - private val glideRequests by lazy { - GlideApp.with(editText) - } + private lateinit var glideRequests: GlideRequests fun setup(editText: EditText) { this.editText = editText + glideRequests = GlideApp.with(editText) val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, R.attr.riotx_background)) setupCommands(backgroundDrawable, editText) setupMembers(backgroundDrawable, editText) @@ -85,6 +87,15 @@ class AutoCompleter @AssistedInject constructor( setupRooms(backgroundDrawable, editText) } + fun clear() { + this.editText = null + autocompleteEmojiPresenter.clear() + autocompleteGroupPresenter.clear() + autocompleteRoomPresenter.clear() + autocompleteCommandPresenter.clear() + autocompleteMemberPresenter.clear() + } + private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) { Autocomplete.on(editText) .with(commandAutocompletePolicy) @@ -104,10 +115,11 @@ class AutoCompleter @AssistedInject constructor( } }) .build() + } private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) { - val autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId) + autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId) Autocomplete.on(editText) .with(CharPolicy('@', true)) .with(autocompleteMemberPresenter) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 3a2665bc99..b993083f35 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -370,6 +370,7 @@ class RoomDetailFragment @Inject constructor( timelineEventController.callback = null timelineEventController.removeModelBuildListener(modelBuildListener) modelBuildListener = null + autoCompleter.clear() debouncer.cancelAll() recyclerView.cleanup() super.onDestroyView()