Commit c24b2fe7 authored by MozLando's avatar MozLando
Browse files

Merge #4655



4655:  For #4653: Do not display suggestion until icon is ready r=pocmo a=sblatz



Co-authored-by: default avatarSawyer Blatz <sdblatz@gmail.com>
Co-authored-by: default avatarSebastian Kaspari <s.kaspari@gmail.com>
parents ffdb27e7 9a9d5e95
Loading
Loading
Loading
Loading
+4 −51
Original line number Diff line number Diff line
@@ -9,12 +9,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.awesomebar.BrowserAwesomeBar
import mozilla.components.browser.awesomebar.R
import mozilla.components.browser.awesomebar.widget.FlowLayout
@@ -34,12 +28,12 @@ internal sealed class DefaultSuggestionViewHolder {
        private val descriptionView = view.findViewById<TextView>(R.id.mozac_browser_awesomebar_description).apply {
            setTextColor(awesomeBar.styling.descriptionTextColor)
        }
        private val iconLoader = IconLoader(view.findViewById(R.id.mozac_browser_awesomebar_icon))
        private val iconView = view.findViewById<ImageView>(R.id.mozac_browser_awesomebar_icon)

        override fun bind(suggestion: AwesomeBar.Suggestion, selectionListener: () -> Unit) {
            val title = if (suggestion.title.isNullOrEmpty()) suggestion.description else suggestion.title

            iconLoader.load(suggestion)
            iconView.setImageBitmap(suggestion.icon)

            titleView.text = title

@@ -56,10 +50,6 @@ internal sealed class DefaultSuggestionViewHolder {
            }
        }

        override fun recycle() {
            iconLoader.cancel()
        }

        companion object {
            val LAYOUT_ID = R.layout.mozac_browser_awesomebar_item_generic
        }
@@ -76,12 +66,12 @@ internal sealed class DefaultSuggestionViewHolder {
            spacing = awesomeBar.styling.chipSpacing
        }
        private val inflater = LayoutInflater.from(view.context)
        private val iconLoader = IconLoader(view.findViewById(R.id.mozac_browser_awesomebar_icon))
        private val iconView = view.findViewById<ImageView>(R.id.mozac_browser_awesomebar_icon)

        override fun bind(suggestion: AwesomeBar.Suggestion, selectionListener: () -> Unit) {
            chipsView.removeAllViews()

            iconLoader.load(suggestion)
            iconView.setImageBitmap(suggestion.icon)

            suggestion
                .chips
@@ -104,45 +94,8 @@ internal sealed class DefaultSuggestionViewHolder {
                }
        }

        override fun recycle() {
            iconLoader.cancel()
        }

        companion object {
            val LAYOUT_ID = R.layout.mozac_browser_awesomebar_item_chips
        }
    }
}

/**
 * Helper class for loading icons asynchronously.
 */
internal class IconLoader(
    private val iconView: ImageView
) {
    private val scope = CoroutineScope(Dispatchers.Main)
    @VisibleForTesting
    internal var iconJob: Job? = null

    fun load(suggestion: AwesomeBar.Suggestion) {
        cancel()

        val icon = suggestion.icon ?: return

        iconJob = scope.launch {
            val bitmap = withContext(Dispatchers.IO) {
                icon.invoke(iconView.measuredWidth, iconView.measuredHeight)
            }

            if (bitmap != null) {
                iconView.setImageBitmap(bitmap)
            }
        }
    }

    fun cancel() {
        iconView.setImageBitmap(null)

        iconJob?.cancel()
    }
}
+0 −87
Original line number Diff line number Diff line
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.browser.awesomebar.layout

import android.graphics.Bitmap
import android.widget.ImageView
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import mozilla.components.concept.awesomebar.AwesomeBar
import mozilla.components.support.test.mock
import mozilla.utils.setupTestCoroutinesDispatcher
import mozilla.utils.unsetTestCoroutinesDispatcher
import org.junit.After
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify

@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class IconLoaderTest {

    @Before
    fun setUp() {
        setupTestCoroutinesDispatcher()
    }

    @After
    fun tearDown() {
        unsetTestCoroutinesDispatcher()
    }

    @Test
    fun `loads image and sets it on ImageView`() {
        runBlocking {
            val bitmap: Bitmap = mock()
            val suggestion = AwesomeBar.Suggestion(
                provider = mock(),
                icon = { _, _ -> bitmap })

            val view: ImageView = mock()
            val loader = IconLoader(view)

            loader.load(suggestion)

            assertNotNull(loader.iconJob)
            loader.iconJob!!.join()

            verify(view).setImageBitmap(bitmap)
        }
    }

    @Test
    fun `Load task is cancelled`() {
        runBlocking {
            @Suppress("UNREACHABLE_CODE")
            val suggestion = AwesomeBar.Suggestion(
                provider = mock(),
                icon = { _, _ ->
                    while (true) { delay(10) }
                    null
                })

            val view: ImageView = mock()
            val loader = IconLoader(view)

            delay(100)

            loader.load(suggestion)

            loader.cancel()

            withTimeout(5000) {
                loader.iconJob!!.join()
            }

            assertTrue(loader.iconJob!!.isCancelled)
        }
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -84,7 +84,7 @@ interface AwesomeBar {
        val id: String = UUID.randomUUID().toString(),
        val title: String? = null,
        val description: String? = null,
        val icon: (suspend (width: Int, height: Int) -> Bitmap?)? = null,
        val icon: Bitmap? = null,
        val chips: List<Chip> = emptyList(),
        val flags: Set<Flag> = emptySet(),
        val onSuggestionClicked: (() -> Unit)? = null,
+1 −1
Original line number Diff line number Diff line
@@ -108,7 +108,7 @@ class AwesomeBarFeature(
        context: Context,
        loadUrlUseCase: SessionUseCases.LoadUrlUseCase
    ): AwesomeBarFeature {
        awesomeBar.addProviders(ClipboardSuggestionProvider(context, loadUrlUseCase, icons = icons))
        awesomeBar.addProviders(ClipboardSuggestionProvider(context, loadUrlUseCase))
        return this
    }

+0 −28
Original line number Diff line number Diff line
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.feature.awesomebar.internal

import android.graphics.Bitmap
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.icons.IconRequest

/**
 * Helper that creates a suspendable lambda to either load a static bitmap ([default]) or from [BrowserIcons] if not
 * null.
 */
internal fun BrowserIcons?.loadLambda(
    url: String,
    default: Bitmap? = null
): (suspend (width: Int, height: Int) -> Bitmap?)? {
    if (default != null) {
        return { _, _ -> default }
    }

    if (this != null) {
        return { _, _ -> loadIcon(IconRequest(url)).await().bitmap }
    }

    return null
}
Loading