diff --git a/app/metrics.yaml b/app/metrics.yaml
index 2ccba8ad4cc..77c7ae22414 100644
--- a/app/metrics.yaml
+++ b/app/metrics.yaml
@@ -1900,3 +1900,57 @@ metrics:
metadata:
tags:
- Performance
+ search_widget_installed:
+ type: boolean
+ lifetime: application
+ description: |
+ Whether or not the search widget is installed
+ send_in_pings:
+ - metrics
+ bugs:
+ - https://github.com/mozilla-mobile/focus-android/issues/
+ data_reviews:
+ - https://github.com/mozilla-mobile/focus-android/pull/7474
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - android-probes@mozilla.com
+ expires: 119
+ metadata:
+ tags:
+ - Search
+
+search_widget:
+ new_tab_button:
+ type: event
+ description: |
+ A user pressed anywhere from the Focus logo until the start of the
+ microphone icon, opening a new tab search screen.
+ bugs:
+ - https://github.com/mozilla-mobile/focus-android/issues/
+ data_reviews:
+ - https://github.com/mozilla-mobile/focus-android/pull/7474
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - android-probes@mozilla.com
+ expires: 119
+ metadata:
+ tags:
+ - Search
+ voice_button:
+ type: event
+ description: |
+ A user pressed the microphone icon, opening a new voice search screen.
+ bugs:
+ - https://github.com/mozilla-mobile/focus-android/issues/
+ data_reviews:
+ - https://github.com/mozilla-mobile/focus-android/pull/7474
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - android-probes@mozilla.com
+ expires: 119
+ metadata:
+ tags:
+ - Search
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1fdc53df28f..a195687dac0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -131,6 +131,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/org/mozilla/focus/activity/IntentReceiverActivity.kt b/app/src/main/java/org/mozilla/focus/activity/IntentReceiverActivity.kt
index a919bc93499..7011cd40b67 100644
--- a/app/src/main/java/org/mozilla/focus/activity/IntentReceiverActivity.kt
+++ b/app/src/main/java/org/mozilla/focus/activity/IntentReceiverActivity.kt
@@ -8,7 +8,10 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import mozilla.components.feature.intent.ext.sanitize
+import mozilla.components.feature.search.widget.BaseVoiceSearchActivity
+import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.utils.toSafeIntent
+import org.mozilla.focus.GleanMetrics.SearchWidget
import org.mozilla.focus.ext.components
import org.mozilla.focus.session.IntentProcessor
import org.mozilla.focus.utils.SupportUtils
@@ -25,14 +28,15 @@ class IntentReceiverActivity : Activity() {
super.onCreate(savedInstanceState)
val intent = intent.sanitize().toSafeIntent()
-
+ if (intent.getBooleanExtra(SEARCH_WIDGET, false)) {
+ SearchWidget.newTabButton.record(NoExtras())
+ }
if (intent.dataString.equals(SupportUtils.OPEN_WITH_DEFAULT_BROWSER_URL)) {
dispatchNormalIntent()
return
}
val result = intentProcessor.handleIntent(this, intent, savedInstanceState)
-
if (result is IntentProcessor.Result.CustomTab) {
dispatchCustomTabsIntent(result.id)
} else {
@@ -43,6 +47,7 @@ class IntentReceiverActivity : Activity() {
}
private fun dispatchCustomTabsIntent(tabId: String) {
+
val intent = Intent(intent)
intent.setClassName(applicationContext, CustomTabActivity::class.java.name)
@@ -58,7 +63,15 @@ class IntentReceiverActivity : Activity() {
val intent = Intent(intent)
intent.setClassName(applicationContext, MainActivity::class.java.name)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
-
+ intent.putExtra(SEARCH_WIDGET, intent.getBooleanExtra(SEARCH_WIDGET, false))
+ intent.putExtra(
+ BaseVoiceSearchActivity.SPEECH_PROCESSING,
+ intent.getStringExtra(BaseVoiceSearchActivity.SPEECH_PROCESSING)
+ )
startActivity(intent)
}
+
+ companion object {
+ const val SEARCH_WIDGET = "search_widget"
+ }
}
diff --git a/app/src/main/java/org/mozilla/focus/activity/MainActivity.kt b/app/src/main/java/org/mozilla/focus/activity/MainActivity.kt
index 00d257d6784..4fba7475322 100644
--- a/app/src/main/java/org/mozilla/focus/activity/MainActivity.kt
+++ b/app/src/main/java/org/mozilla/focus/activity/MainActivity.kt
@@ -21,7 +21,9 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.isVisible
import androidx.preference.PreferenceManager
import mozilla.components.browser.state.selector.privateTabs
+import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.EngineView
+import mozilla.components.feature.search.widget.BaseVoiceSearchActivity
import mozilla.components.lib.auth.canUseBiometricFeature
import mozilla.components.lib.crash.Crash
import mozilla.components.service.glean.private.NoExtras
@@ -51,6 +53,7 @@ import org.mozilla.focus.state.Screen
import org.mozilla.focus.telemetry.TelemetryWrapper
import org.mozilla.focus.telemetry.startuptelemetry.StartupPathProvider
import org.mozilla.focus.telemetry.startuptelemetry.StartupTypeTelemetry
+import org.mozilla.focus.utils.SearchUtils
import org.mozilla.focus.utils.StatusBarUtils
import org.mozilla.focus.utils.SupportUtils
@@ -78,7 +81,6 @@ open class MainActivity : LocaleAwareAppCompatActivity() {
updateSecureWindowFlags()
super.onCreate(savedInstanceState)
-
_binding = ActivityMainBinding.inflate(layoutInflater)
// Checks if Activity is currently in PiP mode if launched from external intents, then exits it
@@ -113,12 +115,8 @@ open class MainActivity : LocaleAwareAppCompatActivity() {
}
val safeIntent = SafeIntent(intent)
- val isTheFirstLaunch = settings.getAppLaunchCount() == 0
- if (isTheFirstLaunch) {
- setSplashScreenPreDrawListener(safeIntent)
- } else {
- showFirstScreen(safeIntent)
- }
+
+ handleSearchWidgetNavigation(safeIntent)
if (intent.hasExtra(HomeScreen.ADD_TO_HOMESCREEN_TAG)) {
intentProcessor.handleNewIntent(this, safeIntent)
@@ -139,6 +137,27 @@ open class MainActivity : LocaleAwareAppCompatActivity() {
AppReviewUtils.showAppReview(this)
}
+ private fun handleSearchWidgetNavigation(safeIntent: SafeIntent) {
+ val voiceSearchText = safeIntent.getStringExtra(BaseVoiceSearchActivity.SPEECH_PROCESSING)
+ if (!voiceSearchText.isNullOrEmpty()) {
+ openVoiceSearchBrowser(voiceSearchText)
+ return
+ }
+
+ val searchWidgetIntent = safeIntent.getBooleanExtra(IntentReceiverActivity.SEARCH_WIDGET, false)
+ if (searchWidgetIntent) {
+ showHomeScreen()
+ return
+ }
+
+ val isTheFirstLaunch = settings.getAppLaunchCount() == 0
+ if (isTheFirstLaunch) {
+ setSplashScreenPreDrawListener(safeIntent)
+ } else {
+ showFirstScreen(safeIntent)
+ }
+ }
+
private fun setSplashScreenPreDrawListener(safeIntent: SafeIntent) {
val endTime = System.currentTimeMillis() + REQUEST_TIME_OUT
binding.container.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
@@ -155,6 +174,26 @@ open class MainActivity : LocaleAwareAppCompatActivity() {
)
}
+ private fun openVoiceSearchBrowser(voiceSearchText: String) {
+ val tabId = this.components.tabsUseCases.addTab(
+ url = SearchUtils.createSearchUrl(
+ this,
+ voiceSearchText
+ ),
+ source = SessionState.Source.External.ActionSend(null),
+ searchTerms = voiceSearchText,
+ selectTab = true,
+ private = true
+ )
+ components.appStore.dispatch(AppAction.OpenTab(tabId))
+ lifecycle.addObserver(navigator)
+ }
+
+ private fun showHomeScreen() {
+ components.appStore.dispatch(AppAction.ShowHomeScreen)
+ lifecycle.addObserver(navigator)
+ }
+
private fun showFirstScreen(safeIntent: SafeIntent) {
// The performance check was added after the shouldShowFirstRun to take as much of the
// code path as possible
diff --git a/app/src/main/java/org/mozilla/focus/searchwidget/SearchWidgetProvider.kt b/app/src/main/java/org/mozilla/focus/searchwidget/SearchWidgetProvider.kt
new file mode 100644
index 00000000000..f33c1b9052f
--- /dev/null
+++ b/app/src/main/java/org/mozilla/focus/searchwidget/SearchWidgetProvider.kt
@@ -0,0 +1,63 @@
+/* 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.focus.searchwidget
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.annotation.VisibleForTesting
+import mozilla.components.feature.search.widget.AppSearchWidgetProvider
+import mozilla.components.feature.search.widget.BaseVoiceSearchActivity
+import mozilla.components.feature.search.widget.SearchWidgetConfig
+import mozilla.components.support.utils.PendingIntentUtils
+import org.mozilla.focus.R
+import org.mozilla.focus.activity.IntentReceiverActivity
+import org.mozilla.focus.ext.components
+
+class SearchWidgetProvider : AppSearchWidgetProvider() {
+
+ override fun onEnabled(context: Context) {
+ context.components.settings.addSearchWidgetInstalled(1)
+ }
+
+ override fun onDeleted(context: Context, appWidgetIds: IntArray) {
+ context.components.settings.addSearchWidgetInstalled(-appWidgetIds.size)
+ }
+
+ override val config: SearchWidgetConfig =
+ SearchWidgetConfig(
+ searchWidgetIconResource = R.drawable.ic_splash_screen,
+ searchWidgetMicrophoneResource = R.drawable.mozac_ic_microphone,
+ appName = R.string.app_name
+ )
+
+ 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(IntentReceiverActivity.SEARCH_WIDGET, true)
+ }
+ return PendingIntent.getActivity(
+ context,
+ REQUEST_CODE_NEW_TAB,
+ textSearchIntent,
+ PendingIntentUtils.defaultFlags or
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ }
+
+ override fun shouldShowVoiceSearch(context: Context): Boolean {
+ return true
+ }
+
+ override fun voiceSearchActivity(): Class {
+ return VoiceSearchActivity::class.java
+ }
+
+ companion object {
+ @VisibleForTesting
+ const val REQUEST_CODE_NEW_TAB = 0
+ }
+}
diff --git a/app/src/main/java/org/mozilla/focus/searchwidget/VoiceSearchActivity.kt b/app/src/main/java/org/mozilla/focus/searchwidget/VoiceSearchActivity.kt
new file mode 100644
index 00000000000..20d0ff61244
--- /dev/null
+++ b/app/src/main/java/org/mozilla/focus/searchwidget/VoiceSearchActivity.kt
@@ -0,0 +1,32 @@
+/* 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.focus.searchwidget
+
+import android.content.Intent
+import mozilla.components.feature.search.widget.BaseVoiceSearchActivity
+import mozilla.components.support.locale.LocaleManager
+import mozilla.components.support.locale.LocaleManager.getCurrentLocale
+import mozilla.telemetry.glean.private.NoExtras
+import org.mozilla.focus.GleanMetrics.SearchWidget
+import org.mozilla.focus.activity.IntentReceiverActivity
+import java.util.Locale
+
+class VoiceSearchActivity : BaseVoiceSearchActivity() {
+
+ override fun getCurrentLocale(): Locale {
+ return getCurrentLocale(this)
+ ?: LocaleManager.getSystemDefault()
+ }
+
+ override fun onSpeechRecognitionEnded(spokenText: String) {
+ val intent = Intent(this, IntentReceiverActivity::class.java)
+ intent.putExtra(SPEECH_PROCESSING, spokenText)
+ startActivity(intent)
+ }
+
+ override fun onSpeechRecognitionStarted() {
+ SearchWidget.voiceButton.record(NoExtras())
+ }
+}
diff --git a/app/src/main/java/org/mozilla/focus/telemetry/GleanMetricsService.kt b/app/src/main/java/org/mozilla/focus/telemetry/GleanMetricsService.kt
index 6167217c31c..4c109452ce0 100644
--- a/app/src/main/java/org/mozilla/focus/telemetry/GleanMetricsService.kt
+++ b/app/src/main/java/org/mozilla/focus/telemetry/GleanMetricsService.kt
@@ -24,6 +24,7 @@ import org.mozilla.focus.Components
import org.mozilla.focus.GleanMetrics.Browser
import org.mozilla.focus.GleanMetrics.GleanBuildInfo
import org.mozilla.focus.GleanMetrics.LegacyIds
+import org.mozilla.focus.GleanMetrics.Metrics
import org.mozilla.focus.GleanMetrics.MozillaProducts
import org.mozilla.focus.GleanMetrics.Pings
import org.mozilla.focus.GleanMetrics.Preferences
@@ -75,7 +76,7 @@ class GleanMetricsService(context: Context) : MetricsService {
GlobalScope.launch(IO) {
// Wait for preferences to be collected before we send the activation ping.
- collectPrefMetrics(components, settings, context).await()
+ collectPrefMetricsAsync(components, settings, context).await()
// Set the client ID in Glean as part of the deletion-request.
LegacyIds.clientId.set(UUID.fromString(TelemetryWrapper.clientId))
@@ -90,7 +91,7 @@ class GleanMetricsService(context: Context) : MetricsService {
}
}
- private fun collectPrefMetrics(
+ private fun collectPrefMetricsAsync(
components: Components,
settings: Settings,
context: Context
@@ -100,6 +101,8 @@ class GleanMetricsService(context: Context) : MetricsService {
val isFenixDefaultBrowser = FenixProductDetector.isFenixDefaultBrowser(installedBrowsers.defaultBrowser)
val isFocusDefaultBrowser = installedBrowsers.isDefaultBrowser
+ Metrics.searchWidgetInstalled.set(settings.searchWidgetInstalled)
+
Browser.isDefault.set(isFocusDefaultBrowser)
Browser.localeOverride.set(components.store.state.locale?.displayName ?: "none")
val shortcutsOnHomeNumber = components.topSitesStorage.getTopSites(
diff --git a/app/src/main/java/org/mozilla/focus/utils/Settings.kt b/app/src/main/java/org/mozilla/focus/utils/Settings.kt
index 6062cae2b80..386fe271220 100644
--- a/app/src/main/java/org/mozilla/focus/utils/Settings.kt
+++ b/app/src/main/java/org/mozilla/focus/utils/Settings.kt
@@ -374,6 +374,20 @@ class Settings(
.commit()
}
+ fun addSearchWidgetInstalled(count: Int) {
+ val key = getPreferenceKey(R.string.pref_key_search_widget_installed)
+ val newValue = preferences.getInt(key, 0) + count
+ preferences.edit()
+ .putInt(key, newValue)
+ .apply()
+ }
+
+ val searchWidgetInstalled: Boolean
+ get() = 0 < preferences.getInt(
+ getPreferenceKey(R.string.pref_key_search_widget_installed),
+ 0
+ )
+
fun getHttpsOnlyMode(): Engine.HttpsOnlyMode {
return if (preferences.getBoolean(getPreferenceKey(R.string.pref_key_https_only), true)) {
Engine.HttpsOnlyMode.ENABLED
diff --git a/app/src/main/res/drawable-hdpi/focus_search_widget.png b/app/src/main/res/drawable-hdpi/focus_search_widget.png
new file mode 100644
index 00000000000..2f967151f4e
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/focus_search_widget.png differ
diff --git a/app/src/main/res/drawable-night-hdpi/focus_search_widget.png b/app/src/main/res/drawable-night-hdpi/focus_search_widget.png
new file mode 100644
index 00000000000..ddf77786d8a
Binary files /dev/null and b/app/src/main/res/drawable-night-hdpi/focus_search_widget.png differ
diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml
index 9416400e42b..8eb1b5acb13 100644
--- a/app/src/main/res/values/preference_keys.xml
+++ b/app/src/main/res/values/preference_keys.xml
@@ -69,6 +69,8 @@
security_category
+ pref_key_search_widget_installed
+
use_homescreen_tips
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 24595b6808a..80f3093d1eb 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -36,6 +36,15 @@
- @style/AppTheme
+
+