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 + +