From a2bace4a33089785b5a6a8a4d169090a2c6263b3 Mon Sep 17 00:00:00 2001 From: iorgamgabriel Date: Mon, 1 Aug 2022 17:06:02 +0300 Subject: [PATCH] For #12565 Implement the common part of search widget in Android Components --- .buildconfig.yml | 4 + components/feature/searchwidget/build.gradle | 36 +++ .../feature/searchwidget/proguard-rules.pro | 21 ++ .../searchwidget/src/main/AndroidManifest.xml | 10 + .../searchwidget/AppSearchWidgetProvider.kt | 274 ++++++++++++++++++ .../feature/searchwidget/IntentUtils.kt | 23 ++ .../searchwidget/VoiceSearchActivity.kt | 126 ++++++++ .../res/drawable-hdpi/mozac_search_widget.png | Bin 0 -> 24278 bytes .../drawable/mozac_ic_launcher_foreground.xml | 83 ++++++ .../drawable/mozac_ic_microphone_widget.xml | 16 + .../mozac_ic_microphone_widget_padded.xml | 6 + ...mozac_rounded_search_widget_background.xml | 9 + .../mozac_search_widget_extra_small_v1.xml | 22 ++ .../mozac_search_widget_extra_small_v2.xml | 21 ++ .../res/layout/mozac_search_widget_large.xml | 43 +++ .../res/layout/mozac_search_widget_medium.xml | 43 +++ .../res/layout/mozac_search_widget_small.xml | 27 ++ .../mozac_search_widget_small_no_mic.xml | 19 ++ .../src/main/res/values-night/colors.xml | 10 + .../src/main/res/values/colors.xml | 9 + .../src/main/res/values/dimens.xml | 4 + .../src/main/res/values/strings.xml | 14 + .../main/res/xml/mozac_search_widget_info.xml | 15 + .../AppSearchWidgetProviderTest.kt | 146 ++++++++++ taskcluster/ci/config.yml | 1 + 25 files changed, 982 insertions(+) create mode 100644 components/feature/searchwidget/build.gradle create mode 100644 components/feature/searchwidget/proguard-rules.pro create mode 100644 components/feature/searchwidget/src/main/AndroidManifest.xml create mode 100644 components/feature/searchwidget/src/main/java/mozilla/components/feature/searchwidget/AppSearchWidgetProvider.kt create mode 100644 components/feature/searchwidget/src/main/java/mozilla/components/feature/searchwidget/IntentUtils.kt create mode 100644 components/feature/searchwidget/src/main/java/mozilla/components/feature/searchwidget/VoiceSearchActivity.kt create mode 100644 components/feature/searchwidget/src/main/res/drawable-hdpi/mozac_search_widget.png create mode 100644 components/feature/searchwidget/src/main/res/drawable/mozac_ic_launcher_foreground.xml create mode 100644 components/feature/searchwidget/src/main/res/drawable/mozac_ic_microphone_widget.xml create mode 100644 components/feature/searchwidget/src/main/res/drawable/mozac_ic_microphone_widget_padded.xml create mode 100644 components/feature/searchwidget/src/main/res/drawable/mozac_rounded_search_widget_background.xml create mode 100644 components/feature/searchwidget/src/main/res/layout/mozac_search_widget_extra_small_v1.xml create mode 100644 components/feature/searchwidget/src/main/res/layout/mozac_search_widget_extra_small_v2.xml create mode 100644 components/feature/searchwidget/src/main/res/layout/mozac_search_widget_large.xml create mode 100644 components/feature/searchwidget/src/main/res/layout/mozac_search_widget_medium.xml create mode 100644 components/feature/searchwidget/src/main/res/layout/mozac_search_widget_small.xml create mode 100644 components/feature/searchwidget/src/main/res/layout/mozac_search_widget_small_no_mic.xml create mode 100644 components/feature/searchwidget/src/main/res/values-night/colors.xml create mode 100644 components/feature/searchwidget/src/main/res/values/colors.xml create mode 100644 components/feature/searchwidget/src/main/res/values/dimens.xml create mode 100644 components/feature/searchwidget/src/main/res/values/strings.xml create mode 100644 components/feature/searchwidget/src/main/res/xml/mozac_search_widget_info.xml create mode 100644 components/feature/searchwidget/src/test/java/mozilla/components/feature/searchwidget/AppSearchWidgetProviderTest.kt diff --git a/.buildconfig.yml b/.buildconfig.yml index 6c29d3afd4e..1c0bbbeb050 100644 --- a/.buildconfig.yml +++ b/.buildconfig.yml @@ -112,6 +112,10 @@ projects: path: components/feature/search description: 'Feature implementation connecting an engine implementation with the search module.' publish: true + feature-searchwidget: + path: components/feature/searchwidget + description: 'Feature implementation for Search Widget' + publish: true feature-serviceworker: path: components/feature/serviceworker description: 'Feature that adds support for service workers when using GeckoEngine.' diff --git a/components/feature/searchwidget/build.gradle b/components/feature/searchwidget/build.gradle new file mode 100644 index 00000000000..090f97081fa --- /dev/null +++ b/components/feature/searchwidget/build.gradle @@ -0,0 +1,36 @@ +/* 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/. */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion config.compileSdkVersion + + defaultConfig { + minSdkVersion config.minSdkVersion + targetSdkVersion config.targetSdkVersion + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation project(':support-base') + implementation project(':ui-colors') + implementation Dependencies.androidx_core_ktx + + testImplementation project(':support-test') + testImplementation Dependencies.androidx_test_core + testImplementation Dependencies.androidx_test_junit + testImplementation Dependencies.testing_robolectric + testImplementation Dependencies.testing_mockito +} +apply from: '../../../publish.gradle' +ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) diff --git a/components/feature/searchwidget/proguard-rules.pro b/components/feature/searchwidget/proguard-rules.pro new file mode 100644 index 00000000000..481bb434814 --- /dev/null +++ b/components/feature/searchwidget/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/components/feature/searchwidget/src/main/AndroidManifest.xml b/components/feature/searchwidget/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..2b116f1096a --- /dev/null +++ b/components/feature/searchwidget/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/components/feature/searchwidget/src/main/java/mozilla/components/feature/searchwidget/AppSearchWidgetProvider.kt b/components/feature/searchwidget/src/main/java/mozilla/components/feature/searchwidget/AppSearchWidgetProvider.kt new file mode 100644 index 00000000000..a86ad0f64d7 --- /dev/null +++ b/components/feature/searchwidget/src/main/java/mozilla/components/feature/searchwidget/AppSearchWidgetProvider.kt @@ -0,0 +1,274 @@ +/* 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.searchwidget + +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.searchwidget.VoiceSearchActivity.Companion.SPEECH_PROCESSING + +abstract class AppSearchWidgetProvider : AppWidgetProvider() { + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + val textSearchIntent = createTextSearchIntent(context) + val voiceSearchIntent = createVoiceSearchIntent(context) + + appWidgetIds.forEach { appWidgetId -> + updateWidgetLayout( + context = context, + appWidgetId = appWidgetId, + appWidgetManager = appWidgetManager, + voiceSearchIntent = voiceSearchIntent, + textSearchIntent = textSearchIntent + ) + } + } + + override fun onAppWidgetOptionsChanged( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + newOptions: Bundle? + ) { + val textSearchIntent = createTextSearchIntent(context) + val voiceSearchIntent = createVoiceSearchIntent(context) + + updateWidgetLayout( + context = context, + appWidgetId = appWidgetId, + appWidgetManager = appWidgetManager, + voiceSearchIntent = voiceSearchIntent, + textSearchIntent = textSearchIntent + ) + } + + /** + * Builds pending intent that opens the browser and starts a new text search. + */ + abstract fun createTextSearchIntent(context: Context): PendingIntent + + /** + * If the microphone will appear on the Search Widget and the user can perform a voice search + */ + abstract fun shouldShowVoiceSearch(context: Context): Boolean + + /** + * Activity that extends VoiceSearchActivity + */ + abstract fun voiceSearchActivity(): VoiceSearchActivity + + /** + * Builds pending intent that starts a new voice search. + */ + @VisibleForTesting + internal fun createVoiceSearchIntent(context: Context): PendingIntent? { + if (!shouldShowVoiceSearch(context)) { + 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 updateWidgetLayout( + context: Context, + appWidgetId: Int, + appWidgetManager: AppWidgetManager, + voiceSearchIntent: PendingIntent?, + textSearchIntent: PendingIntent + ) { + 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) + } + + 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.mozac_search_widget_extra_small_v1, + R.layout.mozac_search_widget_extra_small_v2, + R.layout.mozac_search_widget_small_no_mic -> { + setOnClickPendingIntent( + R.id.mozac_button_search_widget_new_tab, + textSearchIntent + ) + } + R.layout.mozac_search_widget_small -> { + setOnClickPendingIntent( + R.id.mozac_button_search_widget_new_tab, + textSearchIntent + ) + setOnClickPendingIntent( + R.id.mozac_button_search_widget_voice, + voiceSearchIntent + ) + } + R.layout.mozac_search_widget_medium, + R.layout.mozac_search_widget_large -> { + setOnClickPendingIntent( + R.id.mozac_button_search_widget_new_tab, + textSearchIntent + ) + setOnClickPendingIntent( + R.id.mozac_button_search_widget_voice, + voiceSearchIntent + ) + setOnClickPendingIntent( + R.id.mozac_button_search_widget_new_tab_icon, + textSearchIntent + ) + setTextViewText(R.id.mozac_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.mozac_button_search_widget_voice, View.GONE) + } + } + } + } + } + + 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.mozac_button_search_widget_new_tab_icon, + R.drawable.mozac_ic_launcher_foreground + ) + } else { + setImageViewBitmap( + R.id.mozac_button_search_widget_new_tab_icon, + AppCompatResources.getDrawable( + context, + R.drawable.mozac_ic_launcher_foreground + )?.toBitmap() + ) + } + + val appName = context.getString(R.string.app_name) + setContentDescription( + R.id.mozac_button_search_widget_new_tab_icon, + context.getString(R.string.search_widget_content_description, appName) + ) + } + + // 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_VOICE = 1 + + fun updateAllWidgets(context: Context, widgetClassNameApp: AppSearchWidgetProvider) { + val widgetManager = AppWidgetManager.getInstance(context) + val widgetIds = widgetManager.getAppWidgetIds( + ComponentName( + context, + widgetClassNameApp::class.java + ) + ) + + if (widgetIds.isNotEmpty()) { + context.sendBroadcast( + Intent(context, widgetClassNameApp::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.mozac_search_widget_large + SearchWidgetProviderSize.MEDIUM -> R.layout.mozac_search_widget_medium + SearchWidgetProviderSize.SMALL -> { + if (showMic) { + R.layout.mozac_search_widget_small + } else { + R.layout.mozac_search_widget_small_no_mic + } + } + SearchWidgetProviderSize.EXTRA_SMALL_V2 -> R.layout.mozac_search_widget_extra_small_v2 + SearchWidgetProviderSize.EXTRA_SMALL_V1 -> R.layout.mozac_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/components/feature/searchwidget/src/main/java/mozilla/components/feature/searchwidget/IntentUtils.kt b/components/feature/searchwidget/src/main/java/mozilla/components/feature/searchwidget/IntentUtils.kt new file mode 100644 index 00000000000..8613393d574 --- /dev/null +++ b/components/feature/searchwidget/src/main/java/mozilla/components/feature/searchwidget/IntentUtils.kt @@ -0,0 +1,23 @@ +/* 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.searchwidget + +import android.app.PendingIntent +import android.os.Build + +object IntentUtils { + + /** + * Since Android 12 we need to set PendingIntent mutability explicitly, but Android 6 can be the minimum version + * This additional requirement improves your app's security. + * FLAG_IMMUTABLE -> Flag indicating that the created PendingIntent should be immutable. + */ + val defaultIntentPendingFlags + get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_IMMUTABLE + } else { + 0 // No flags. Default behavior. + } +} diff --git a/components/feature/searchwidget/src/main/java/mozilla/components/feature/searchwidget/VoiceSearchActivity.kt b/components/feature/searchwidget/src/main/java/mozilla/components/feature/searchwidget/VoiceSearchActivity.kt new file mode 100644 index 00000000000..fb5630cc07e --- /dev/null +++ b/components/feature/searchwidget/src/main/java/mozilla/components/feature/searchwidget/VoiceSearchActivity.kt @@ -0,0 +1,126 @@ +/* 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.searchwidget + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.speech.RecognizerIntent +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AppCompatActivity +import java.util.Locale + +/** + * Launches voice recognition then uses it to start a new web search. + */ +abstract class VoiceSearchActivity : AppCompatActivity() { + + /** + * Holds the intent that initially started this activity + * so that it can persist through the speech activity. + */ + private var previousIntent: Intent? = null + + @VisibleForTesting + private var activityResultLauncher: ActivityResultLauncher? = null + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putParcelable(PREVIOUS_INTENT, previousIntent) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + activityResultLauncher = getActivityResultLauncher() + 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() + } + } + + /** + * Language locale for Voice Search + */ + abstract fun getCurrentLocale(): Locale + + /** + *records telemetry when speech recognizer popup is shown + */ + abstract fun recordVoiceButtonTelemetry() + + /** + *start intent after voice search for example a browser page is open with the spokenText + * @param spokenText what the user voice search + */ + abstract fun startIntentAfterVoiceSearch(spokenText: String?) + + @VisibleForTesting + private fun getActivityResultLauncher(): ActivityResultLauncher { + return registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == Activity.RESULT_OK) { + val spokenText = + it.data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.first() + previousIntent?.apply { + startIntentAfterVoiceSearch(spokenText) + } + } + finish() + } + } + + /** + * Displays a speech recognizer popup that listens for input from the user. + */ + 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, + getCurrentLocale() + ) + } + recordVoiceButtonTelemetry() + activityResultLauncher?.launch(intentSpeech) + } + + /** + * 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 { + const val PREVIOUS_INTENT = "org.mozilla.components.previous_intent" + + /** + * In [VoiceSearchActivity] activity, used to store if the speech processing should start. + */ + const val SPEECH_PROCESSING = "speech_processing" + } +} diff --git a/components/feature/searchwidget/src/main/res/drawable-hdpi/mozac_search_widget.png b/components/feature/searchwidget/src/main/res/drawable-hdpi/mozac_search_widget.png new file mode 100644 index 0000000000000000000000000000000000000000..216234c1483f7d07517e7353ad93f2cee1f6d9e5 GIT binary patch literal 24278 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

rqm!vL{sQ)Z~U3^`u+(*9rn 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 literal 0 HcmV?d00001 diff --git a/components/feature/searchwidget/src/main/res/drawable/mozac_ic_launcher_foreground.xml b/components/feature/searchwidget/src/main/res/drawable/mozac_ic_launcher_foreground.xml new file mode 100644 index 00000000000..844e479ef48 --- /dev/null +++ b/components/feature/searchwidget/src/main/res/drawable/mozac_ic_launcher_foreground.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/feature/searchwidget/src/main/res/drawable/mozac_ic_microphone_widget.xml b/components/feature/searchwidget/src/main/res/drawable/mozac_ic_microphone_widget.xml new file mode 100644 index 00000000000..53e709cd81f --- /dev/null +++ b/components/feature/searchwidget/src/main/res/drawable/mozac_ic_microphone_widget.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/components/feature/searchwidget/src/main/res/drawable/mozac_ic_microphone_widget_padded.xml b/components/feature/searchwidget/src/main/res/drawable/mozac_ic_microphone_widget_padded.xml new file mode 100644 index 00000000000..eaaea7dc809 --- /dev/null +++ b/components/feature/searchwidget/src/main/res/drawable/mozac_ic_microphone_widget_padded.xml @@ -0,0 +1,6 @@ + + diff --git a/components/feature/searchwidget/src/main/res/drawable/mozac_rounded_search_widget_background.xml b/components/feature/searchwidget/src/main/res/drawable/mozac_rounded_search_widget_background.xml new file mode 100644 index 00000000000..d4cd59c1a52 --- /dev/null +++ b/components/feature/searchwidget/src/main/res/drawable/mozac_rounded_search_widget_background.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_extra_small_v1.xml b/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_extra_small_v1.xml new file mode 100644 index 00000000000..f9cf5d3089f --- /dev/null +++ b/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_extra_small_v1.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_extra_small_v2.xml b/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_extra_small_v2.xml new file mode 100644 index 00000000000..7d78096f131 --- /dev/null +++ b/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_extra_small_v2.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_large.xml b/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_large.xml new file mode 100644 index 00000000000..7044e42e340 --- /dev/null +++ b/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_large.xml @@ -0,0 +1,43 @@ + + + + + + + + + + diff --git a/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_medium.xml b/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_medium.xml new file mode 100644 index 00000000000..6b021ea0748 --- /dev/null +++ b/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_medium.xml @@ -0,0 +1,43 @@ + + + + + + + + + + diff --git a/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_small.xml b/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_small.xml new file mode 100644 index 00000000000..da75440cf20 --- /dev/null +++ b/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_small.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_small_no_mic.xml b/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_small_no_mic.xml new file mode 100644 index 00000000000..c838fc6277b --- /dev/null +++ b/components/feature/searchwidget/src/main/res/layout/mozac_search_widget_small_no_mic.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/components/feature/searchwidget/src/main/res/values-night/colors.xml b/components/feature/searchwidget/src/main/res/values-night/colors.xml new file mode 100644 index 00000000000..d74cbd0201c --- /dev/null +++ b/components/feature/searchwidget/src/main/res/values-night/colors.xml @@ -0,0 +1,10 @@ + + + + @color/photonDarkGrey60 + @color/photonLightGrey05 + @color/photonLightGrey05 + + diff --git a/components/feature/searchwidget/src/main/res/values/colors.xml b/components/feature/searchwidget/src/main/res/values/colors.xml new file mode 100644 index 00000000000..dc6e846772a --- /dev/null +++ b/components/feature/searchwidget/src/main/res/values/colors.xml @@ -0,0 +1,9 @@ + + + + @color/photonLightGrey10 + @color/photonDarkGrey90 + @color/photonDarkGrey90 + diff --git a/components/feature/searchwidget/src/main/res/values/dimens.xml b/components/feature/searchwidget/src/main/res/values/dimens.xml new file mode 100644 index 00000000000..d9611e74f1c --- /dev/null +++ b/components/feature/searchwidget/src/main/res/values/dimens.xml @@ -0,0 +1,4 @@ + + + 8dp + diff --git a/components/feature/searchwidget/src/main/res/values/strings.xml b/components/feature/searchwidget/src/main/res/values/strings.xml new file mode 100644 index 00000000000..6e43e8720fe --- /dev/null +++ b/components/feature/searchwidget/src/main/res/values/strings.xml @@ -0,0 +1,14 @@ + + + Search Widget + + + + Open a new %1$s tab + + Search + + Search the web + + Voice search + diff --git a/components/feature/searchwidget/src/main/res/xml/mozac_search_widget_info.xml b/components/feature/searchwidget/src/main/res/xml/mozac_search_widget_info.xml new file mode 100644 index 00000000000..fe4de999c7b --- /dev/null +++ b/components/feature/searchwidget/src/main/res/xml/mozac_search_widget_info.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/components/feature/searchwidget/src/test/java/mozilla/components/feature/searchwidget/AppSearchWidgetProviderTest.kt b/components/feature/searchwidget/src/test/java/mozilla/components/feature/searchwidget/AppSearchWidgetProviderTest.kt new file mode 100644 index 00000000000..2a9763a47bc --- /dev/null +++ b/components/feature/searchwidget/src/test/java/mozilla/components/feature/searchwidget/AppSearchWidgetProviderTest.kt @@ -0,0 +1,146 @@ +/* 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.searchwidget + +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doReturn +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class AppSearchWidgetProviderTest { + + @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, AppSearchWidgetProvider.getLayoutSize(dp)) + } + } + + @Test + fun testGetLargeLayout() { + assertEquals( + R.layout.mozac_search_widget_large, + AppSearchWidgetProvider.getLayout(SearchWidgetProviderSize.LARGE, showMic = false) + ) + assertEquals( + R.layout.mozac_search_widget_large, + AppSearchWidgetProvider.getLayout(SearchWidgetProviderSize.LARGE, showMic = true) + ) + } + + @Test + fun testGetMediumLayout() { + assertEquals( + R.layout.mozac_search_widget_medium, + AppSearchWidgetProvider.getLayout(SearchWidgetProviderSize.MEDIUM, showMic = false) + ) + assertEquals( + R.layout.mozac_search_widget_medium, + AppSearchWidgetProvider.getLayout(SearchWidgetProviderSize.MEDIUM, showMic = true) + ) + } + + @Test + fun testGetSmallLayout() { + assertEquals( + R.layout.mozac_search_widget_small_no_mic, + AppSearchWidgetProvider.getLayout(SearchWidgetProviderSize.SMALL, showMic = false) + ) + assertEquals( + R.layout.mozac_search_widget_small, + AppSearchWidgetProvider.getLayout(SearchWidgetProviderSize.SMALL, showMic = true) + ) + } + + @Test + fun testGetExtraSmall2Layout() { + assertEquals( + R.layout.mozac_search_widget_extra_small_v2, + AppSearchWidgetProvider.getLayout( + SearchWidgetProviderSize.EXTRA_SMALL_V2, + showMic = false + ) + ) + assertEquals( + R.layout.mozac_search_widget_extra_small_v2, + AppSearchWidgetProvider.getLayout( + SearchWidgetProviderSize.EXTRA_SMALL_V2, + showMic = true + ) + ) + } + + @Test + fun testGetExtraSmall1Layout() { + assertEquals( + R.layout.mozac_search_widget_extra_small_v1, + AppSearchWidgetProvider.getLayout( + SearchWidgetProviderSize.EXTRA_SMALL_V1, + showMic = false + ) + ) + assertEquals( + R.layout.mozac_search_widget_extra_small_v1, + AppSearchWidgetProvider.getLayout( + SearchWidgetProviderSize.EXTRA_SMALL_V1, + showMic = true + ) + ) + } + + @Test + fun testGetText() { + assertEquals( + testContext.getString(R.string.search_widget_text_long), + AppSearchWidgetProvider.getText(SearchWidgetProviderSize.LARGE, testContext) + ) + assertEquals( + testContext.getString(R.string.search_widget_text_short), + AppSearchWidgetProvider.getText(SearchWidgetProviderSize.MEDIUM, testContext) + ) + assertNull(AppSearchWidgetProvider.getText(SearchWidgetProviderSize.SMALL, testContext)) + assertNull( + AppSearchWidgetProvider.getText( + SearchWidgetProviderSize.EXTRA_SMALL_V1, + testContext + ) + ) + assertNull( + AppSearchWidgetProvider.getText( + SearchWidgetProviderSize.EXTRA_SMALL_V2, + testContext + ) + ) + } + + @Test + fun `GIVEN voice search is disabled WHEN createVoiceSearchIntent is called THEN it returns null`() { + val appSearchWidgetProvider: AppSearchWidgetProvider = mock() + doReturn(false).`when`(appSearchWidgetProvider).shouldShowVoiceSearch(testContext) + + val result = appSearchWidgetProvider.createVoiceSearchIntent(testContext) + + assertNull(result) + } +} diff --git a/taskcluster/ci/config.yml b/taskcluster/ci/config.yml index 12d81ff37ba..2627ea97b93 100644 --- a/taskcluster/ci/config.yml +++ b/taskcluster/ci/config.yml @@ -56,6 +56,7 @@ treeherder: feature-readerview: feature-readerview feature-recentlyclosed: feature-recentlyclosed feature-search: feature-search + feature-searchwidget: feature-searchwidget feature-serviceworker: feature-serviceworker feature-session: feature-session feature-share: feature-share