diff --git a/app/src/main/java/org/mozilla/gecko/search/SearchWidgetProvider.kt b/app/src/main/java/org/mozilla/gecko/search/SearchWidgetProvider.kt index f933b7c4f..b3650ff4d 100644 --- a/app/src/main/java/org/mozilla/gecko/search/SearchWidgetProvider.kt +++ b/app/src/main/java/org/mozilla/gecko/search/SearchWidgetProvider.kt @@ -17,6 +17,7 @@ import android.view.View import android.widget.RemoteViews import androidx.annotation.Dimension import androidx.annotation.Dimension.DP +import androidx.annotation.VisibleForTesting import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.drawable.toBitmap import org.mozilla.fenix.HomeActivity @@ -27,7 +28,6 @@ import org.mozilla.fenix.home.intent.StartSearchIntentProcessor import org.mozilla.fenix.widget.VoiceSearchActivity import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.SPEECH_PROCESSING -@Suppress("TooManyFunctions") class SearchWidgetProvider : AppWidgetProvider() { // Implementation note: // This class name (SearchWidgetProvider) and package name (org.mozilla.gecko.search) should @@ -79,31 +79,9 @@ class SearchWidgetProvider : AppWidgetProvider() { appWidgetManager.updateAppWidget(appWidgetId, views) } - private fun getLayoutSize(@Dimension(unit = DP) dp: Int) = when { - dp >= DP_LARGE -> SearchWidgetProviderSize.LARGE - dp >= DP_MEDIUM -> SearchWidgetProviderSize.MEDIUM - dp >= DP_SMALL -> SearchWidgetProviderSize.SMALL - dp >= DP_EXTRA_SMALL -> SearchWidgetProviderSize.EXTRA_SMALL_V2 - else -> SearchWidgetProviderSize.EXTRA_SMALL_V1 - } - - private fun getLayout(size: SearchWidgetProviderSize, showMic: Boolean) = when (size) { - SearchWidgetProviderSize.LARGE -> R.layout.search_widget_large - SearchWidgetProviderSize.MEDIUM -> R.layout.search_widget_medium - SearchWidgetProviderSize.SMALL -> { - if (showMic) R.layout.search_widget_small - else R.layout.search_widget_small_no_mic - } - SearchWidgetProviderSize.EXTRA_SMALL_V2 -> R.layout.search_widget_extra_small_v2 - SearchWidgetProviderSize.EXTRA_SMALL_V1 -> R.layout.search_widget_extra_small_v1 - } - - private fun getText(layout: SearchWidgetProviderSize, context: Context) = when (layout) { - SearchWidgetProviderSize.MEDIUM -> context.getString(R.string.search_widget_text_short) - SearchWidgetProviderSize.LARGE -> context.getString(R.string.search_widget_text_long) - else -> null - } - + /** + * Builds pending intent that opens the browser and starts a new text search. + */ private fun createTextSearchIntent(context: Context): PendingIntent { return Intent(context, IntentReceiverActivity::class.java) .let { intent -> @@ -114,6 +92,9 @@ class SearchWidgetProvider : AppWidgetProvider() { } } + /** + * Builds pending intent that starts a new voice search. + */ private fun createVoiceSearchIntent(context: Context): PendingIntent? { val voiceIntent = Intent(context, VoiceSearchActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK @@ -156,7 +137,7 @@ class SearchWidgetProvider : AppWidgetProvider() { // Unlike "small" widget, "medium" and "large" sizes do not have separate layouts // that exclude the microphone icon, which is why we must hide it accordingly here. if (voiceSearchIntent == null) { - this.setViewVisibility(R.id.button_search_widget_voice, View.GONE) + setViewVisibility(R.id.button_search_widget_voice, View.GONE) } } } @@ -187,6 +168,40 @@ class SearchWidgetProvider : AppWidgetProvider() { private const val DP_LARGE = 256 private const val REQUEST_CODE_NEW_TAB = 0 private const val REQUEST_CODE_VOICE = 1 + + @VisibleForTesting + internal fun getLayoutSize(@Dimension(unit = DP) dp: Int) = when { + dp >= DP_LARGE -> SearchWidgetProviderSize.LARGE + dp >= DP_MEDIUM -> SearchWidgetProviderSize.MEDIUM + dp >= DP_SMALL -> SearchWidgetProviderSize.SMALL + dp >= DP_EXTRA_SMALL -> SearchWidgetProviderSize.EXTRA_SMALL_V2 + else -> SearchWidgetProviderSize.EXTRA_SMALL_V1 + } + + /** + * Get the layout resource to use for the search widget. + */ + @VisibleForTesting + internal fun getLayout(size: SearchWidgetProviderSize, showMic: Boolean) = when (size) { + SearchWidgetProviderSize.LARGE -> R.layout.search_widget_large + SearchWidgetProviderSize.MEDIUM -> R.layout.search_widget_medium + SearchWidgetProviderSize.SMALL -> { + if (showMic) R.layout.search_widget_small + else R.layout.search_widget_small_no_mic + } + SearchWidgetProviderSize.EXTRA_SMALL_V2 -> R.layout.search_widget_extra_small_v2 + SearchWidgetProviderSize.EXTRA_SMALL_V1 -> R.layout.search_widget_extra_small_v1 + } + + /** + * Get the text to place in the search widget + */ + @VisibleForTesting + internal fun getText(layout: SearchWidgetProviderSize, context: Context) = when (layout) { + SearchWidgetProviderSize.MEDIUM -> context.getString(R.string.search_widget_text_short) + SearchWidgetProviderSize.LARGE -> context.getString(R.string.search_widget_text_long) + else -> null + } } } diff --git a/app/src/test/java/org/mozilla/fenix/widget/SearchWidgetProviderTest.kt b/app/src/test/java/org/mozilla/fenix/widget/SearchWidgetProviderTest.kt new file mode 100644 index 000000000..34ff21d76 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/widget/SearchWidgetProviderTest.kt @@ -0,0 +1,118 @@ +/* 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 org.mozilla.fenix.widget + +import android.content.Context +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.mozilla.fenix.R +import org.mozilla.gecko.search.SearchWidgetProvider +import org.mozilla.gecko.search.SearchWidgetProviderSize + +class SearchWidgetProviderTest { + + @Test + fun testGetLayoutSize() { + val sizes = mapOf( + 0 to SearchWidgetProviderSize.EXTRA_SMALL_V1, + 10 to SearchWidgetProviderSize.EXTRA_SMALL_V1, + 63 to SearchWidgetProviderSize.EXTRA_SMALL_V1, + 64 to SearchWidgetProviderSize.EXTRA_SMALL_V2, + 99 to SearchWidgetProviderSize.EXTRA_SMALL_V2, + 100 to SearchWidgetProviderSize.SMALL, + 191 to SearchWidgetProviderSize.SMALL, + 192 to SearchWidgetProviderSize.MEDIUM, + 255 to SearchWidgetProviderSize.MEDIUM, + 256 to SearchWidgetProviderSize.LARGE, + 1000 to SearchWidgetProviderSize.LARGE + ) + + for ((dp, layoutSize) in sizes) { + assertEquals(layoutSize, SearchWidgetProvider.getLayoutSize(dp)) + } + } + + @Test + fun testGetLargeLayout() { + assertEquals( + R.layout.search_widget_large, + SearchWidgetProvider.getLayout(SearchWidgetProviderSize.LARGE, showMic = false) + ) + assertEquals( + R.layout.search_widget_large, + SearchWidgetProvider.getLayout(SearchWidgetProviderSize.LARGE, showMic = true) + ) + } + + @Test + fun testGetMediumLayout() { + assertEquals( + R.layout.search_widget_medium, + SearchWidgetProvider.getLayout(SearchWidgetProviderSize.MEDIUM, showMic = false) + ) + assertEquals( + R.layout.search_widget_medium, + SearchWidgetProvider.getLayout(SearchWidgetProviderSize.MEDIUM, showMic = true) + ) + } + + @Test + fun testGetSmallLayout() { + assertEquals( + R.layout.search_widget_small_no_mic, + SearchWidgetProvider.getLayout(SearchWidgetProviderSize.SMALL, showMic = false) + ) + assertEquals( + R.layout.search_widget_small, + SearchWidgetProvider.getLayout(SearchWidgetProviderSize.SMALL, showMic = true) + ) + } + + @Test + fun testGetExtraSmall2Layout() { + assertEquals( + R.layout.search_widget_extra_small_v2, + SearchWidgetProvider.getLayout(SearchWidgetProviderSize.EXTRA_SMALL_V2, showMic = false) + ) + assertEquals( + R.layout.search_widget_extra_small_v2, + SearchWidgetProvider.getLayout(SearchWidgetProviderSize.EXTRA_SMALL_V2, showMic = true) + ) + } + + @Test + fun testGetExtraSmall1Layout() { + assertEquals( + R.layout.search_widget_extra_small_v1, + SearchWidgetProvider.getLayout(SearchWidgetProviderSize.EXTRA_SMALL_V1, showMic = false) + ) + assertEquals( + R.layout.search_widget_extra_small_v1, + SearchWidgetProvider.getLayout(SearchWidgetProviderSize.EXTRA_SMALL_V1, showMic = true) + ) + } + + @Test + fun testGetText() { + val context = mockk() + every { context.getString(R.string.search_widget_text_short) } returns "Search" + every { context.getString(R.string.search_widget_text_long) } returns "Search the web" + + assertEquals( + "Search the web", + SearchWidgetProvider.getText(SearchWidgetProviderSize.LARGE, context) + ) + assertEquals( + "Search", + SearchWidgetProvider.getText(SearchWidgetProviderSize.MEDIUM, context) + ) + assertNull(SearchWidgetProvider.getText(SearchWidgetProviderSize.SMALL, context)) + assertNull(SearchWidgetProvider.getText(SearchWidgetProviderSize.EXTRA_SMALL_V1, context)) + assertNull(SearchWidgetProvider.getText(SearchWidgetProviderSize.EXTRA_SMALL_V2, context)) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/widget/VoiceSearchActivityTest.kt b/app/src/test/java/org/mozilla/fenix/widget/VoiceSearchActivityTest.kt index 11292bbfe..8327f869e 100644 --- a/app/src/test/java/org/mozilla/fenix/widget/VoiceSearchActivityTest.kt +++ b/app/src/test/java/org/mozilla/fenix/widget/VoiceSearchActivityTest.kt @@ -73,6 +73,32 @@ class VoiceSearchActivityTest { assertTrue(activity.isFinishing) } + @Test + fun `process null intent`() { + val controller = Robolectric.buildActivity(VoiceSearchActivity::class.java, null) + val activity = controller.get() + + controller.create() + + assertTrue(activity.isFinishing) + } + + @Test + fun `save previous intent to instance state`() { + val previousIntent = Intent().apply { + putExtra(SPEECH_PROCESSING, true) + } + val savedInstanceState = Bundle().apply { + putParcelable(PREVIOUS_INTENT, previousIntent) + } + val outState = Bundle() + + controller.create(savedInstanceState) + controller.saveInstanceState(outState) + + assertEquals(previousIntent, outState.getParcelable(PREVIOUS_INTENT) as Intent) + } + @Test fun `process intent with speech processing in previous intent set to true`() { val savedInstanceState = Bundle() @@ -107,4 +133,18 @@ class VoiceSearchActivityTest { assertEquals("hello world", browserIntent.getStringExtra(SPEECH_PROCESSING)) assertTrue(browserIntent.getBooleanExtra(OPEN_TO_BROWSER_AND_LOAD, false)) } + + @Test + fun `handle invalid result code`() { + controller.create() + + val resultIntent = Intent() + shadow.receiveResult( + shadow.peekNextStartedActivityForResult().intent, + Activity.RESULT_CANCELED, + resultIntent + ) + + assertTrue(activity.isFinishing) + } }