From 69e3aa229d28579910600940dfeebc6c772b7503 Mon Sep 17 00:00:00 2001 From: iorgamgabriel Date: Fri, 5 Aug 2022 17:18:20 +0300 Subject: [PATCH] For #26329 Refactor the search widget to use the A-C common behaviour --- .../org/mozilla/fenix/FenixApplication.kt | 32 ++- .../intent/SpeechProcessingIntentProcessor.kt | 2 +- .../fenix/search/SearchDialogFragment.kt | 7 +- .../settings/search/SearchEngineFragment.kt | 3 +- .../java/org/mozilla/fenix/utils/Settings.kt | 8 + .../fenix/widget/VoiceSearchActivity.kt | 112 ++------ .../gecko/search/SearchWidgetProvider.kt | 241 +++--------------- .../drawable/ic_microphone_widget_padded.xml | 6 - .../rounded_search_widget_background.xml | 9 - .../layout/search_widget_extra_small_v1.xml | 22 -- .../layout/search_widget_extra_small_v2.xml | 21 -- .../main/res/layout/search_widget_large.xml | 43 ---- .../main/res/layout/search_widget_medium.xml | 43 ---- .../main/res/layout/search_widget_small.xml | 27 -- .../res/layout/search_widget_small_no_mic.xml | 19 -- app/src/main/res/values/preference_keys.xml | 1 + app/src/main/res/values/strings.xml | 3 +- app/src/main/res/xml/search_widget_info.xml | 2 +- .../SpeechProcessingIntentProcessorTest.kt | 2 +- .../search/SearchEngineFragmentTest.kt | 1 - .../fenix/widget/SearchWidgetProviderTest.kt | 121 +-------- .../fenix/widget/VoiceSearchActivityTest.kt | 192 -------------- 22 files changed, 94 insertions(+), 823 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_microphone_widget_padded.xml delete mode 100644 app/src/main/res/drawable/rounded_search_widget_background.xml delete mode 100644 app/src/main/res/layout/search_widget_extra_small_v1.xml delete mode 100644 app/src/main/res/layout/search_widget_extra_small_v2.xml delete mode 100644 app/src/main/res/layout/search_widget_large.xml delete mode 100644 app/src/main/res/layout/search_widget_medium.xml delete mode 100644 app/src/main/res/layout/search_widget_small.xml delete mode 100644 app/src/main/res/layout/search_widget_small_no_mic.xml delete mode 100644 app/src/test/java/org/mozilla/fenix/widget/VoiceSearchActivityTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index 848ec3a1fd3e..b01671d3b8b7 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -37,6 +37,7 @@ import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider import mozilla.components.feature.autofill.AutofillUseCases import mozilla.components.feature.search.ext.buildSearchUrl import mozilla.components.feature.search.ext.waitForSelectedOrDefaultSearchEngine +import mozilla.components.feature.search.widget.AppSearchWidgetProvider import mozilla.components.feature.top.sites.TopSitesFrecencyConfig import mozilla.components.feature.top.sites.TopSitesProviderConfig import mozilla.components.lib.crash.CrashReporter @@ -44,7 +45,6 @@ import mozilla.components.service.fxa.manager.SyncEnginesStorage import mozilla.components.service.glean.Glean import mozilla.components.service.glean.config.Configuration import mozilla.components.service.glean.net.ConceptFetchHttpUploader -import mozilla.components.support.rusterrors.initializeRustErrors import mozilla.components.support.base.facts.register import mozilla.components.support.base.log.Log import mozilla.components.support.base.log.logger.Logger @@ -52,6 +52,7 @@ import mozilla.components.support.base.observer.Observable import mozilla.components.support.ktx.android.content.isMainProcess import mozilla.components.support.ktx.android.content.runOnlyInMainProcess import mozilla.components.support.locale.LocaleAwareApplication +import mozilla.components.support.rusterrors.initializeRustErrors import mozilla.components.support.rusthttp.RustHttpConfig import mozilla.components.support.rustlog.RustLog import mozilla.components.support.utils.logElapsedTime @@ -93,6 +94,7 @@ import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD import org.mozilla.fenix.wallpapers.WallpaperManager +import org.mozilla.gecko.search.SearchWidgetProvider import java.util.UUID import java.util.concurrent.TimeUnit @@ -543,8 +545,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider { WebExtensionSupport.initialize( components.core.engine, components.core.store, - onNewTabOverride = { - _, engineSession, url -> + onNewTabOverride = { _, engineSession, url -> val shouldCreatePrivateSession = components.core.store.state.selectedTab?.content?.private ?: components.settings.openLinksInAPrivateTab @@ -556,12 +557,10 @@ open class FenixApplication : LocaleAwareApplication(), Provider { private = shouldCreatePrivateSession ) }, - onCloseTabOverride = { - _, sessionId -> + onCloseTabOverride = { _, sessionId -> components.useCases.tabsUseCases.removeTab(sessionId) }, - onSelectTabOverride = { - _, sessionId -> + onSelectTabOverride = { _, sessionId -> components.useCases.tabsUseCases.selectTab(sessionId) }, onExtensionsLoaded = { extensions -> @@ -627,7 +626,11 @@ open class FenixApplication : LocaleAwareApplication(), Provider { TopSites.contextId.set(UUID.fromString(settings.contileContextId)) } - mozillaProducts.set(mozillaProductDetector.getInstalledMozillaProducts(applicationContext)) + mozillaProducts.set( + mozillaProductDetector.getInstalledMozillaProducts( + applicationContext + ) + ) adjustCampaign.set(settings.adjustCampaignId) adjustAdGroup.set(settings.adjustAdGroup) @@ -717,6 +720,19 @@ open class FenixApplication : LocaleAwareApplication(), Provider { } } } + updateSearchWidgetProvider() + } + + // This update is needed to prevent the icons disappearing from the old version of search widget + // the was implemented only in Fenix to the current version with AC. + private fun updateSearchWidgetProvider() { + if (components.settings.shouldUpdateSearchWidget) { + AppSearchWidgetProvider.updateAllWidgets( + this, + SearchWidgetProvider::class.java + ) + components.settings.shouldUpdateSearchWidget = false + } } @Suppress("ComplexMethod") diff --git a/app/src/main/java/org/mozilla/fenix/home/intent/SpeechProcessingIntentProcessor.kt b/app/src/main/java/org/mozilla/fenix/home/intent/SpeechProcessingIntentProcessor.kt index bf97b4f289b9..72b4ee7882be 100644 --- a/app/src/main/java/org/mozilla/fenix/home/intent/SpeechProcessingIntentProcessor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/intent/SpeechProcessingIntentProcessor.kt @@ -11,11 +11,11 @@ import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine import mozilla.components.browser.state.store.BrowserStore import mozilla.components.feature.search.ext.waitForSelectedOrDefaultSearchEngine +import mozilla.components.feature.search.widget.BaseVoiceSearchActivity.Companion.SPEECH_PROCESSING import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.components.metrics.MetricsUtils import org.mozilla.fenix.ext.components -import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.SPEECH_PROCESSING /** * The search widget has a microphone button to let users search with their voice. diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt index 264417eff2d9..9e7d7e1d3f3e 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt @@ -37,7 +37,6 @@ import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider @@ -88,7 +87,6 @@ import org.mozilla.fenix.search.toolbar.SearchSelectorMenu import org.mozilla.fenix.search.toolbar.SearchSelectorToolbarAction import org.mozilla.fenix.search.toolbar.ToolbarView import org.mozilla.fenix.settings.SupportUtils -import org.mozilla.fenix.widget.VoiceSearchActivity typealias SearchDialogFragmentStore = SearchFragmentStore @@ -549,7 +547,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { } override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { - if (requestCode == VoiceSearchActivity.SPEECH_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + if (requestCode == SPEECH_REQUEST_CODE && resultCode == Activity.RESULT_OK) { intent?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.first()?.also { toolbarView.view.edit.updateUrl(url = it, shouldHighlight = true) interactor.onTextChanged(it) @@ -790,7 +788,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) putExtra(RecognizerIntent.EXTRA_PROMPT, requireContext().getString(R.string.voice_search_explainer)) } - startActivityForResult(speechIntent, VoiceSearchActivity.SPEECH_REQUEST_CODE) + startActivityForResult(speechIntent, SPEECH_REQUEST_CODE) } private fun updateQrButton(searchFragmentState: SearchFragmentState) { @@ -904,6 +902,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { companion object { private const val TAP_INCREASE_DPS = 8 + const val SPEECH_REQUEST_CODE = 0 private const val QR_FRAGMENT_TAG = "MOZAC_QR_FRAGMENT" private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1 } diff --git a/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt index 4cd6fb15b8ab..a16ad879fe90 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt @@ -11,6 +11,7 @@ import androidx.preference.CheckBoxPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreference +import mozilla.components.feature.search.widget.AppSearchWidgetProvider.Companion.updateAllWidgets import mozilla.components.support.ktx.android.view.hideKeyboard import org.mozilla.fenix.R import org.mozilla.fenix.ext.getPreferenceKey @@ -90,7 +91,7 @@ class SearchEngineFragment : PreferenceFragmentCompat() { requireContext().settings().preferences.edit { putBoolean(preference.key, newBooleanValue) } - SearchWidgetProvider.updateAllWidgets(requireContext()) + updateAllWidgets(requireContext(), SearchWidgetProvider::class.java) return true } } diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index 500c5c8e446b..8b915eaba260 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -316,6 +316,14 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = false ) + var shouldUpdateSearchWidget: Boolean + get() = preferences.getBoolean(appContext.getPreferenceKey(R.string.pref_key_update_search_widget), true) + set(value) { + preferences.edit() + .putBoolean(appContext.getPreferenceKey(R.string.pref_key_update_search_widget), value) + .apply() + } + var gridTabView by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_tab_view_grid), default = true diff --git a/app/src/main/java/org/mozilla/fenix/widget/VoiceSearchActivity.kt b/app/src/main/java/org/mozilla/fenix/widget/VoiceSearchActivity.kt index 5a763b60eb33..5bd0b656ed17 100644 --- a/app/src/main/java/org/mozilla/fenix/widget/VoiceSearchActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/widget/VoiceSearchActivity.kt @@ -4,118 +4,38 @@ package org.mozilla.fenix.widget -import android.content.ComponentName import android.content.Intent -import android.os.Bundle import android.os.StrictMode -import android.speech.RecognizerIntent -import androidx.appcompat.app.AppCompatActivity +import mozilla.components.feature.search.widget.BaseVoiceSearchActivity import mozilla.components.support.locale.LocaleManager import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.GleanMetrics.SearchWidget import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.ext.components +import java.util.Locale /** - * Launches voice recognition then uses it to start a new web search. + * Implementation of voice search that is needed in search widget */ -class VoiceSearchActivity : AppCompatActivity() { +class VoiceSearchActivity : BaseVoiceSearchActivity() { - /** - * Holds the intent that initially started this activity - * so that it can persist through the speech activity. - */ - private var previousIntent: Intent? = null - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putParcelable(PREVIOUS_INTENT, previousIntent) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - if (Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).resolveActivity(packageManager) == null) { - finish() - return - } - - // Retrieve the previous intent from the saved state - previousIntent = savedInstanceState?.get(PREVIOUS_INTENT) as Intent? - if (previousIntent.isForSpeechProcessing()) { - // Don't reopen the speech recognizer - return - } - - // The intent property is nullable, but the rest of the code below assumes it is not. - val intent = intent?.let { Intent(intent) } ?: Intent() - - if (intent.isForSpeechProcessing()) { - previousIntent = intent - displaySpeechRecognizer() - } else { - finish() - } - } - - /** - * Displays a speech recognizer popup that listens for input from the user. - */ - @Suppress("DEPRECATION") - // https://github.com/mozilla-mobile/fenix/issues/19919 - private fun displaySpeechRecognizer() { - val intentSpeech = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { - putExtra( - RecognizerIntent.EXTRA_LANGUAGE_MODEL, - RecognizerIntent.LANGUAGE_MODEL_FREE_FORM - ) - putExtra( - RecognizerIntent.EXTRA_LANGUAGE, - components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { - LocaleManager.getCurrentLocale(this@VoiceSearchActivity) - } - ) + override fun getCurrentLocale(): Locale { + val locale = components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { + LocaleManager.getCurrentLocale(this@VoiceSearchActivity) + ?: LocaleManager.getSystemDefault() } - SearchWidget.voiceButton.record(NoExtras()) - - startActivityForResult(intentSpeech, SPEECH_REQUEST_CODE) + return locale } - @Suppress("DEPRECATION") - // https://github.com/mozilla-mobile/fenix/issues/19919 - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - if (requestCode == SPEECH_REQUEST_CODE && resultCode == RESULT_OK) { - val spokenText = data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.first() - val context = this - - previousIntent?.apply { - component = ComponentName(context, IntentReceiverActivity::class.java) - putExtra(SPEECH_PROCESSING, spokenText) - putExtra(HomeActivity.OPEN_TO_BROWSER_AND_LOAD, true) - startActivity(this) - } - } - - finish() + override fun onSpeechRecognitionEnded(spokenText: String) { + val intent = Intent(this, IntentReceiverActivity::class.java) + intent.putExtra(SPEECH_PROCESSING, spokenText) + intent.putExtra(HomeActivity.OPEN_TO_BROWSER_AND_LOAD, true) + startActivity(intent) } - /** - * Returns true if the [SPEECH_PROCESSING] extra is present and set to true. - * Returns false if the intent is null. - */ - private fun Intent?.isForSpeechProcessing(): Boolean = - this?.getBooleanExtra(SPEECH_PROCESSING, false) == true - - companion object { - internal const val SPEECH_REQUEST_CODE = 0 - internal const val PREVIOUS_INTENT = "org.mozilla.fenix.previous_intent" - /** - * In [VoiceSearchActivity] activity, used to store if the speech processing should start. - * In [IntentReceiverActivity] activity, used to store the search terms. - */ - const val SPEECH_PROCESSING = "speech_processing" + override fun onSpeechRecognitionStarted() { + SearchWidget.voiceButton.record(NoExtras()) } } 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 c02594f7ae63..76f35a1275cb 100644 --- a/app/src/main/java/org/mozilla/gecko/search/SearchWidgetProvider.kt +++ b/app/src/main/java/org/mozilla/gecko/search/SearchWidgetProvider.kt @@ -5,22 +5,11 @@ package org.mozilla.gecko.search import android.app.PendingIntent -import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH -import android.appwidget.AppWidgetProvider -import android.content.ComponentName import android.content.Context import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.speech.RecognizerIntent -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 mozilla.components.feature.search.widget.AppSearchWidgetProvider +import mozilla.components.feature.search.widget.BaseVoiceSearchActivity +import mozilla.components.feature.search.widget.SearchWidgetConfig import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.R @@ -28,9 +17,12 @@ import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.intent.StartSearchIntentProcessor import org.mozilla.fenix.utils.IntentUtils import org.mozilla.fenix.widget.VoiceSearchActivity -import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.SPEECH_PROCESSING -class SearchWidgetProvider : AppWidgetProvider() { +/** + * Implementation of search widget + */ +class SearchWidgetProvider : AppSearchWidgetProvider() { + // Implementation note: // This class name (SearchWidgetProvider) and package name (org.mozilla.gecko.search) should // not be changed because otherwise this widget will disappear from the home screen of the user. @@ -44,209 +36,40 @@ class SearchWidgetProvider : AppWidgetProvider() { context.settings().addSearchWidgetInstalled(-appWidgetIds.size) } - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { - val textSearchIntent = createTextSearchIntent(context) - val voiceSearchIntent = createVoiceSearchIntent(context) - - appWidgetIds.forEach { appWidgetId -> - val currentWidth = appWidgetManager.getAppWidgetOptions(appWidgetId).getInt(OPTION_APPWIDGET_MIN_WIDTH) - val layoutSize = getLayoutSize(currentWidth) - // It's not enough to just hide the microphone on the "small" sized widget due to its design. - // The "small" widget needs a complete redesign, meaning it needs a new layout file. - val showMic = (voiceSearchIntent != null) - val layout = getLayout(layoutSize, showMic) - val text = getText(layoutSize, context) - - val views = createRemoteViews(context, layout, textSearchIntent, voiceSearchIntent, text) - appWidgetManager.updateAppWidget(appWidgetId, views) - } - } - - override fun onAppWidgetOptionsChanged( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetId: Int, - newOptions: Bundle? - ) { - val textSearchIntent = createTextSearchIntent(context) - val voiceSearchIntent = createVoiceSearchIntent(context) - - val currentWidth = appWidgetManager.getAppWidgetOptions(appWidgetId).getInt(OPTION_APPWIDGET_MIN_WIDTH) - val layoutSize = getLayoutSize(currentWidth) - val showMic = (voiceSearchIntent != null) - val layout = getLayout(layoutSize, showMic) - val text = getText(layoutSize, context) - - val views = createRemoteViews(context, layout, textSearchIntent, voiceSearchIntent, text) - appWidgetManager.updateAppWidget(appWidgetId, views) - } + override val config: SearchWidgetConfig = + SearchWidgetConfig( + searchWidgetIconResource = R.drawable.ic_launcher_foreground, + searchWidgetMicrophoneResource = R.drawable.ic_microphone_widget, + appName = R.string.app_name + ) - /** - * 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 -> - val createTextSearchIntentFlags = IntentUtils.defaultIntentPendingFlags or - PendingIntent.FLAG_UPDATE_CURRENT - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - intent.putExtra(HomeActivity.OPEN_TO_SEARCH, StartSearchIntentProcessor.SEARCH_WIDGET) - PendingIntent.getActivity( - context, - REQUEST_CODE_NEW_TAB, intent, createTextSearchIntentFlags + override fun createTextSearchIntent(context: Context): PendingIntent { + val textSearchIntent = Intent(context, IntentReceiverActivity::class.java) + .apply { + this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + this.putExtra( + HomeActivity.OPEN_TO_SEARCH, + StartSearchIntentProcessor.SEARCH_WIDGET ) } + return PendingIntent.getActivity( + context, + REQUEST_CODE_NEW_TAB, + textSearchIntent, + IntentUtils.defaultIntentPendingFlags or + PendingIntent.FLAG_UPDATE_CURRENT + ) } - /** - * Builds pending intent that starts a new voice search. - */ - @VisibleForTesting - internal fun createVoiceSearchIntent(context: Context): PendingIntent? { - if (!context.settings().shouldShowVoiceSearch) { - return null - } - - val voiceIntent = Intent(context, VoiceSearchActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - putExtra(SPEECH_PROCESSING, true) - } - - val intentSpeech = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) - - return intentSpeech.resolveActivity(context.packageManager)?.let { - PendingIntent.getActivity( - context, - REQUEST_CODE_VOICE, voiceIntent, IntentUtils.defaultIntentPendingFlags - ) - } - } - - private fun createRemoteViews( - context: Context, - layout: Int, - textSearchIntent: PendingIntent, - voiceSearchIntent: PendingIntent?, - text: String? - ): RemoteViews { - return RemoteViews(context.packageName, layout).apply { - setIcon(context) - when (layout) { - R.layout.search_widget_extra_small_v1, - R.layout.search_widget_extra_small_v2, - R.layout.search_widget_small_no_mic -> { - setOnClickPendingIntent(R.id.button_search_widget_new_tab, textSearchIntent) - } - R.layout.search_widget_small -> { - setOnClickPendingIntent(R.id.button_search_widget_new_tab, textSearchIntent) - setOnClickPendingIntent(R.id.button_search_widget_voice, voiceSearchIntent) - } - R.layout.search_widget_medium, - R.layout.search_widget_large -> { - setOnClickPendingIntent(R.id.button_search_widget_new_tab, textSearchIntent) - setOnClickPendingIntent(R.id.button_search_widget_voice, voiceSearchIntent) - setOnClickPendingIntent(R.id.button_search_widget_new_tab_icon, textSearchIntent) - setTextViewText(R.id.button_search_widget_new_tab, text) - // 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) { - setViewVisibility(R.id.button_search_widget_voice, View.GONE) - } - } - } - } + override fun shouldShowVoiceSearch(context: Context): Boolean { + return context.settings().shouldShowVoiceSearch } - private fun RemoteViews.setIcon(context: Context) { - // gradient color available for android:fillColor only on SDK 24+ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - setImageViewResource( - R.id.button_search_widget_new_tab_icon, - R.drawable.ic_launcher_foreground - ) - } else { - setImageViewBitmap( - R.id.button_search_widget_new_tab_icon, - AppCompatResources.getDrawable( - context, - R.drawable.ic_launcher_foreground - )?.toBitmap() - ) - } - - val appName = context.getString(R.string.app_name) - setContentDescription( - R.id.button_search_widget_new_tab_icon, - context.getString(R.string.search_widget_content_description_2, appName) - ) + override fun voiceSearchActivity(): Class { + return VoiceSearchActivity::class.java } - // Cell sizes obtained from the actual dimensions listed in search widget specs companion object { - private const val DP_EXTRA_SMALL = 64 - private const val DP_SMALL = 100 - private const val DP_MEDIUM = 192 - private const val DP_LARGE = 256 private const val REQUEST_CODE_NEW_TAB = 0 - private const val REQUEST_CODE_VOICE = 1 - - fun updateAllWidgets(context: Context) { - val widgetManager = AppWidgetManager.getInstance(context) - val widgetIds = widgetManager.getAppWidgetIds(ComponentName(context, SearchWidgetProvider::class.java)) - - if (widgetIds.isNotEmpty()) { - context.sendBroadcast( - Intent(context, SearchWidgetProvider::class.java).apply { - action = AppWidgetManager.ACTION_APPWIDGET_UPDATE - putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds) - } - ) - } - } - - @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 - } } } - -enum class SearchWidgetProviderSize { - EXTRA_SMALL_V1, - EXTRA_SMALL_V2, - SMALL, - MEDIUM, - LARGE, -} diff --git a/app/src/main/res/drawable/ic_microphone_widget_padded.xml b/app/src/main/res/drawable/ic_microphone_widget_padded.xml deleted file mode 100644 index 665b13f1baf0..000000000000 --- a/app/src/main/res/drawable/ic_microphone_widget_padded.xml +++ /dev/null @@ -1,6 +0,0 @@ - - diff --git a/app/src/main/res/drawable/rounded_search_widget_background.xml b/app/src/main/res/drawable/rounded_search_widget_background.xml deleted file mode 100644 index 4cd3c134377d..000000000000 --- a/app/src/main/res/drawable/rounded_search_widget_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/search_widget_extra_small_v1.xml b/app/src/main/res/layout/search_widget_extra_small_v1.xml deleted file mode 100644 index 0362d1392847..000000000000 --- a/app/src/main/res/layout/search_widget_extra_small_v1.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/search_widget_extra_small_v2.xml b/app/src/main/res/layout/search_widget_extra_small_v2.xml deleted file mode 100644 index 6bac1cbcfe4a..000000000000 --- a/app/src/main/res/layout/search_widget_extra_small_v2.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/search_widget_large.xml b/app/src/main/res/layout/search_widget_large.xml deleted file mode 100644 index e17529759bf9..000000000000 --- a/app/src/main/res/layout/search_widget_large.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/search_widget_medium.xml b/app/src/main/res/layout/search_widget_medium.xml deleted file mode 100644 index c51ff6b61372..000000000000 --- a/app/src/main/res/layout/search_widget_medium.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/search_widget_small.xml b/app/src/main/res/layout/search_widget_small.xml deleted file mode 100644 index db0531accb05..000000000000 --- a/app/src/main/res/layout/search_widget_small.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/search_widget_small_no_mic.xml b/app/src/main/res/layout/search_widget_small_no_mic.xml deleted file mode 100644 index d1ae6e58d3fa..000000000000 --- a/app/src/main/res/layout/search_widget_small_no_mic.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 0ab0608b8862..0ad4344a6622 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -104,6 +104,7 @@ pref_key_show_search_suggestions_in_privateonboarding pref_key_show_voice_search pref_key_enable_domain_autocomplete + pref_key_update_search_widget pref_key_show_site_exceptions diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0bea8990ac5c..7d096317de11 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -259,7 +259,8 @@ - Open a new %1$s tab + Open a new %1$s tab + Open a new %1$s tab Search diff --git a/app/src/main/res/xml/search_widget_info.xml b/app/src/main/res/xml/search_widget_info.xml index ce7e44a2a4c7..bb40d415198d 100644 --- a/app/src/main/res/xml/search_widget_info.xml +++ b/app/src/main/res/xml/search_widget_info.xml @@ -10,6 +10,6 @@ android:minResizeWidth="30dp" android:previewImage="@drawable/fenix_search_widget" android:updatePeriodMillis="3600000" - android:initialLayout="@layout/search_widget_large" + android:initialLayout="@layout/mozac_search_widget_large" android:widgetCategory="home_screen"> diff --git a/app/src/test/java/org/mozilla/fenix/home/intent/SpeechProcessingIntentProcessorTest.kt b/app/src/test/java/org/mozilla/fenix/home/intent/SpeechProcessingIntentProcessorTest.kt index f9e9b6ee5c35..451fd6eba0e8 100644 --- a/app/src/test/java/org/mozilla/fenix/home/intent/SpeechProcessingIntentProcessorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/intent/SpeechProcessingIntentProcessorTest.kt @@ -15,6 +15,7 @@ import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.SearchState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.feature.search.ext.createSearchEngine +import mozilla.components.feature.search.widget.BaseVoiceSearchActivity.Companion.SPEECH_PROCESSING import mozilla.components.support.test.robolectric.testContext import org.junit.Before import org.junit.Test @@ -24,7 +25,6 @@ import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.ext.components import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.helpers.perf.TestStrictModeManager -import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.SPEECH_PROCESSING @RunWith(FenixRobolectricTestRunner::class) class SpeechProcessingIntentProcessorTest { diff --git a/app/src/test/java/org/mozilla/fenix/settings/search/SearchEngineFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/settings/search/SearchEngineFragmentTest.kt index c69f4b9fb71d..ac954f6e4367 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/search/SearchEngineFragmentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/search/SearchEngineFragmentTest.kt @@ -99,7 +99,6 @@ class SearchEngineFragmentTest { voiceSearchPreference.callChangeListener(true) verify { preferencesEditor.putBoolean(voiceSearchPreferenceKey, true) } - verify { SearchWidgetProvider.updateAllWidgets(testContext) } } finally { unmockkObject(SearchWidgetProvider.Companion) } diff --git a/app/src/test/java/org/mozilla/fenix/widget/SearchWidgetProviderTest.kt b/app/src/test/java/org/mozilla/fenix/widget/SearchWidgetProviderTest.kt index 6244dc55cf76..34fb563607a2 100644 --- a/app/src/test/java/org/mozilla/fenix/widget/SearchWidgetProviderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/widget/SearchWidgetProviderTest.kt @@ -16,131 +16,16 @@ import io.mockk.mockkStatic import io.mockk.slot import io.mockk.unmockkStatic import io.mockk.verify +import mozilla.components.feature.search.widget.AppSearchWidgetProvider import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith -import org.mozilla.fenix.R -import org.mozilla.fenix.ext.settings import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.gecko.search.SearchWidgetProvider -import org.mozilla.gecko.search.SearchWidgetProviderSize @RunWith(FenixRobolectricTestRunner::class) 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)) - } - - @Test - fun `GIVEN voice search is disabled WHEN createVoiceSearchIntent is called THEN it returns null`() { - val widgetProvider = SearchWidgetProvider() - val context: Context = mockk { - every { settings().shouldShowVoiceSearch } returns false - } - - val result = widgetProvider.createVoiceSearchIntent(context) - - assertNull(result) - } - @Test fun `GIVEN widgets set on screen shown WHEN updateAllWidgets is called THEN it sends a broadcast to update all widgets`() { try { @@ -154,7 +39,7 @@ class SearchWidgetProviderTest { val intentCaptor = slot() every { context.sendBroadcast(capture(intentCaptor)) } just Runs - SearchWidgetProvider.updateAllWidgets(context) + AppSearchWidgetProvider.updateAllWidgets(context, SearchWidgetProvider::class.java) verify { context.sendBroadcast(any()) } assertEquals(SearchWidgetProvider::class.java.name, componentNameCaptor.captured.className) @@ -179,7 +64,7 @@ class SearchWidgetProviderTest { val intentCaptor = slot() every { context.sendBroadcast(capture(intentCaptor)) } just Runs - SearchWidgetProvider.updateAllWidgets(context) + AppSearchWidgetProvider.updateAllWidgets(context, SearchWidgetProvider::class.java) verify(exactly = 0) { context.sendBroadcast(any()) } } finally { diff --git a/app/src/test/java/org/mozilla/fenix/widget/VoiceSearchActivityTest.kt b/app/src/test/java/org/mozilla/fenix/widget/VoiceSearchActivityTest.kt deleted file mode 100644 index cb8fd76f1ba3..000000000000 --- a/app/src/test/java/org/mozilla/fenix/widget/VoiceSearchActivityTest.kt +++ /dev/null @@ -1,192 +0,0 @@ -/* 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.app.Activity -import android.content.ComponentName -import android.content.Intent -import android.content.IntentFilter -import android.os.Bundle -import android.speech.RecognizerIntent.ACTION_RECOGNIZE_SPEECH -import android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL -import android.speech.RecognizerIntent.EXTRA_RESULTS -import android.speech.RecognizerIntent.LANGUAGE_MODEL_FREE_FORM -import androidx.appcompat.app.AppCompatActivity.RESULT_OK -import androidx.test.core.app.ApplicationProvider -import io.mockk.every -import io.mockk.mockk -import mozilla.components.support.test.robolectric.testContext -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mozilla.fenix.FenixApplication -import org.mozilla.fenix.HomeActivity.Companion.OPEN_TO_BROWSER_AND_LOAD -import org.mozilla.fenix.IntentReceiverActivity -import org.mozilla.fenix.ext.components -import org.mozilla.fenix.helpers.FenixRobolectricTestRunner -import org.mozilla.fenix.helpers.perf.TestStrictModeManager -import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.PREVIOUS_INTENT -import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.SPEECH_PROCESSING -import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.SPEECH_REQUEST_CODE -import org.robolectric.Robolectric -import org.robolectric.Shadows.shadowOf -import org.robolectric.android.controller.ActivityController -import org.robolectric.shadows.ShadowActivity - -@RunWith(FenixRobolectricTestRunner::class) -class VoiceSearchActivityTest { - - private lateinit var controller: ActivityController - private lateinit var activity: VoiceSearchActivity - private lateinit var shadow: ShadowActivity - - @Before - fun setup() { - val intent = Intent() - intent.putExtra(SPEECH_PROCESSING, true) - - controller = Robolectric.buildActivity(VoiceSearchActivity::class.java, intent) - activity = controller.get() - shadow = shadowOf(activity) - } - - private fun allowVoiceIntentToResolveActivity() { - val context = ApplicationProvider.getApplicationContext() - val shadowPackageManager = shadowOf(context.packageManager) - val component = ComponentName("com.test", "Test") - shadowPackageManager.addActivityIfNotPresent(component) - shadowPackageManager.addIntentFilterForActivity( - component, - IntentFilter(ACTION_RECOGNIZE_SPEECH).apply { addCategory(Intent.CATEGORY_DEFAULT) } - ) - } - - @Test - fun `process intent with speech processing set to true`() { - every { testContext.components.analytics } returns mockk(relaxed = true) - every { testContext.components.strictMode } returns TestStrictModeManager() - allowVoiceIntentToResolveActivity() - controller.create() - - val intentForResult = shadow.peekNextStartedActivityForResult() - assertEquals(SPEECH_REQUEST_CODE, intentForResult.requestCode) - assertEquals(ACTION_RECOGNIZE_SPEECH, intentForResult.intent.action) - assertEquals( - LANGUAGE_MODEL_FREE_FORM, - intentForResult.intent.getStringExtra(EXTRA_LANGUAGE_MODEL) - ) - } - - @Test - fun `process intent with speech processing set to false`() { - allowVoiceIntentToResolveActivity() - val intent = Intent() - intent.putExtra(SPEECH_PROCESSING, false) - - val controller = Robolectric.buildActivity(VoiceSearchActivity::class.java, intent) - val activity = controller.get() - - controller.create() - - assertTrue(activity.isFinishing) - } - - @Test - fun `process null intent`() { - allowVoiceIntentToResolveActivity() - 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`() { - allowVoiceIntentToResolveActivity() - 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)) - } - - @Test - fun `process intent with speech processing in previous intent set to true`() { - allowVoiceIntentToResolveActivity() - val savedInstanceState = Bundle() - val previousIntent = Intent().apply { - putExtra(SPEECH_PROCESSING, true) - } - savedInstanceState.putParcelable(PREVIOUS_INTENT, previousIntent) - - controller.create(savedInstanceState) - - assertFalse(activity.isFinishing) - assertNull(shadow.peekNextStartedActivityForResult()) - } - - @Test - fun `handle speech result`() { - every { testContext.components.analytics } returns mockk(relaxed = true) - every { testContext.components.strictMode } returns TestStrictModeManager() - allowVoiceIntentToResolveActivity() - controller.create() - - val resultIntent = Intent().apply { - putStringArrayListExtra(EXTRA_RESULTS, arrayListOf("hello world")) - } - shadow.receiveResult( - shadow.peekNextStartedActivityForResult().intent, - RESULT_OK, - resultIntent - ) - - val browserIntent = shadow.peekNextStartedActivity() - - assertTrue(activity.isFinishing) - assertEquals( - ComponentName(activity, IntentReceiverActivity::class.java), - browserIntent.component - ) - assertEquals("hello world", browserIntent.getStringExtra(SPEECH_PROCESSING)) - assertTrue(browserIntent.getBooleanExtra(OPEN_TO_BROWSER_AND_LOAD, false)) - } - - @Test - fun `handle invalid result code`() { - every { testContext.components.analytics } returns mockk(relaxed = true) - every { testContext.components.strictMode } returns TestStrictModeManager() - allowVoiceIntentToResolveActivity() - controller.create() - - val resultIntent = Intent() - shadow.receiveResult( - shadow.peekNextStartedActivityForResult().intent, - Activity.RESULT_CANCELED, - resultIntent - ) - - assertTrue(activity.isFinishing) - } - - @Test - fun `handle no activity able to resolve voice intent`() { - controller.create() - assertTrue(activity.isFinishing) - } -}