For #201: Search Widget
parent
ec96d134ff
commit
83ceec6289
|
@ -108,6 +108,16 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".SearchWidgetProvider">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/search_widget_info" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".components.FirebasePush"
|
android:name=".components.FirebasePush"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
|
|
|
@ -189,6 +189,17 @@ open class HomeActivity : AppCompatActivity(), ShareFragment.TabsSharedCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleOpenedFromExternalSourceIfNecessary(intent: Intent?) {
|
private fun handleOpenedFromExternalSourceIfNecessary(intent: Intent?) {
|
||||||
|
if (intent?.extras?.getBoolean(OPEN_TO_BROWSER_AND_LOAD) == true) {
|
||||||
|
this.intent.putExtra(OPEN_TO_BROWSER_AND_LOAD, false)
|
||||||
|
openToBrowserAndLoad(intent.getStringExtra(
|
||||||
|
IntentReceiverActivity.SPEECH_PROCESSING), true, BrowserDirection.FromGlobal, forceSearch = true)
|
||||||
|
return
|
||||||
|
} else if (intent?.extras?.getBoolean(OPEN_TO_SEARCH) == true) {
|
||||||
|
this.intent.putExtra(OPEN_TO_SEARCH, false)
|
||||||
|
navHost.navController.nav(null, NavGraphDirections.actionGlobalSearch(null, true))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (intent?.extras?.getBoolean(OPEN_TO_BROWSER) != true) return
|
if (intent?.extras?.getBoolean(OPEN_TO_BROWSER) != true) return
|
||||||
|
|
||||||
this.intent.putExtra(OPEN_TO_BROWSER, false)
|
this.intent.putExtra(OPEN_TO_BROWSER, false)
|
||||||
|
@ -391,6 +402,8 @@ open class HomeActivity : AppCompatActivity(), ShareFragment.TabsSharedCallback
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val OPEN_TO_BROWSER = "open_to_browser"
|
const val OPEN_TO_BROWSER = "open_to_browser"
|
||||||
|
const val OPEN_TO_BROWSER_AND_LOAD = "open_to_browser_and_load"
|
||||||
|
const val OPEN_TO_SEARCH = "open_to_search"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ package org.mozilla.fenix
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.speech.RecognizerIntent
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.mozilla.fenix.components.NotificationManager
|
import org.mozilla.fenix.components.NotificationManager
|
||||||
|
@ -17,10 +18,19 @@ import org.mozilla.fenix.utils.Settings
|
||||||
|
|
||||||
class IntentReceiverActivity : Activity() {
|
class IntentReceiverActivity : Activity() {
|
||||||
|
|
||||||
|
// Holds the intent that initially started this activity
|
||||||
|
// so that it can persist through the speech activity.
|
||||||
|
private var previousIntent: Intent? = null
|
||||||
|
|
||||||
@Suppress("ComplexMethod")
|
@Suppress("ComplexMethod")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
previousIntent = savedInstanceState?.get(PREVIOUS_INTENT) as Intent?
|
||||||
|
if (previousIntent?.getBooleanExtra(SPEECH_PROCESSING, false) == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val isPrivate = Settings.getInstance(this).usePrivateMode
|
val isPrivate = Settings.getInstance(this).usePrivateMode
|
||||||
|
|
||||||
MainScope().launch {
|
MainScope().launch {
|
||||||
|
@ -34,12 +44,17 @@ class IntentReceiverActivity : Activity() {
|
||||||
if (isPrivate) components.utils.privateIntentProcessor else components.utils.intentProcessor
|
if (isPrivate) components.utils.privateIntentProcessor else components.utils.intentProcessor
|
||||||
)
|
)
|
||||||
|
|
||||||
intentProcessors.any { it.process(intent) }
|
if (intent.getBooleanExtra(SPEECH_PROCESSING, false)) {
|
||||||
setIntentActivity(intent)
|
previousIntent = intent
|
||||||
|
displaySpeechRecognizer()
|
||||||
|
} else {
|
||||||
|
intentProcessors.any { it.process(intent) }
|
||||||
|
setIntentActivity(intent)
|
||||||
|
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|
||||||
finish()
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,4 +93,42 @@ class IntentReceiverActivity : Activity() {
|
||||||
|
|
||||||
intent.putExtra(HomeActivity.OPEN_TO_BROWSER, openToBrowser)
|
intent.putExtra(HomeActivity.OPEN_TO_BROWSER, openToBrowser)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putParcelable(PREVIOUS_INTENT, previousIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displaySpeechRecognizer() {
|
||||||
|
val intentSpeech = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
|
||||||
|
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
|
||||||
|
}
|
||||||
|
|
||||||
|
startActivityForResult(intentSpeech, SPEECH_REQUEST_CODE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
if (requestCode == SPEECH_REQUEST_CODE && resultCode == RESULT_OK) {
|
||||||
|
val spokenText: String? =
|
||||||
|
data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.let { results ->
|
||||||
|
results[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
previousIntent?.let {
|
||||||
|
it.putExtra(SPEECH_PROCESSING, spokenText)
|
||||||
|
it.putExtra(HomeActivity.OPEN_TO_BROWSER_AND_LOAD, true)
|
||||||
|
startActivity(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SPEECH_REQUEST_CODE = 0
|
||||||
|
const val SPEECH_PROCESSING = "speech_processing"
|
||||||
|
const val PREVIOUS_INTENT = "previous_intent"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
/* 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
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH
|
||||||
|
import android.appwidget.AppWidgetProvider
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
|
||||||
|
class SearchWidgetProvider : AppWidgetProvider() {
|
||||||
|
|
||||||
|
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)
|
||||||
|
val layout = getLayout(layoutSize)
|
||||||
|
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 layout = getLayout(layoutSize)
|
||||||
|
val text = getText(layoutSize, context)
|
||||||
|
|
||||||
|
val views = createRemoteViews(context, layout, textSearchIntent, voiceSearchIntent, text)
|
||||||
|
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLayoutSize(dp: Int) = when {
|
||||||
|
dp >= DP_EXTRA_LARGE -> SearchWidgetProviderSize.EXTRA_LARGE
|
||||||
|
dp >= DP_LARGE -> SearchWidgetProviderSize.LARGE
|
||||||
|
dp >= DP_MEDIUM -> SearchWidgetProviderSize.MEDIUM
|
||||||
|
dp >= DP_SMALL -> SearchWidgetProviderSize.SMALL
|
||||||
|
else -> SearchWidgetProviderSize.EXTRA_SMALL
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLayout(size: SearchWidgetProviderSize) = when (size) {
|
||||||
|
SearchWidgetProviderSize.EXTRA_LARGE -> R.layout.search_widget_extra_large
|
||||||
|
SearchWidgetProviderSize.LARGE -> R.layout.search_widget_large
|
||||||
|
SearchWidgetProviderSize.MEDIUM -> R.layout.search_widget_medium
|
||||||
|
SearchWidgetProviderSize.SMALL -> R.layout.search_widget_small
|
||||||
|
SearchWidgetProviderSize.EXTRA_SMALL -> R.layout.search_widget_extra_small
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getText(layout: SearchWidgetProviderSize, context: Context) = when (layout) {
|
||||||
|
SearchWidgetProviderSize.MEDIUM -> context.getString(R.string.search_widget_text_short)
|
||||||
|
SearchWidgetProviderSize.LARGE,
|
||||||
|
SearchWidgetProviderSize.EXTRA_LARGE -> context.getString(R.string.search_widget_text_long)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTextSearchIntent(context: Context): PendingIntent {
|
||||||
|
return Intent(context, HomeActivity::class.java)
|
||||||
|
.let { intent ->
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
intent.putExtra(HomeActivity.OPEN_TO_SEARCH, true)
|
||||||
|
PendingIntent.getActivity(context, REQUEST_CODE_NEW_TAB, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createVoiceSearchIntent(context: Context): PendingIntent {
|
||||||
|
return Intent(context, IntentReceiverActivity::class.java)
|
||||||
|
.let { intent ->
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
intent.putExtra(IntentReceiverActivity.SPEECH_PROCESSING, true)
|
||||||
|
PendingIntent.getActivity(context, REQUEST_CODE_VOICE, intent, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRemoteViews(
|
||||||
|
context: Context,
|
||||||
|
layout: Int,
|
||||||
|
textSearchIntent: PendingIntent,
|
||||||
|
voiceSearchIntent: PendingIntent,
|
||||||
|
text: String?
|
||||||
|
): RemoteViews {
|
||||||
|
return RemoteViews(context.packageName, layout).apply {
|
||||||
|
when (layout) {
|
||||||
|
R.layout.search_widget_extra_small -> {
|
||||||
|
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,
|
||||||
|
R.layout.search_widget_extra_large -> {
|
||||||
|
setOnClickPendingIntent(R.id.button_search_widget_new_tab, textSearchIntent)
|
||||||
|
setOnClickPendingIntent(R.id.button_search_widget_voice, voiceSearchIntent)
|
||||||
|
setTextViewText(R.id.text_search_widget, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cell sizes obtained from the actual dimensions listed in search widget specs
|
||||||
|
companion object {
|
||||||
|
private const val DP_SMALL = 100
|
||||||
|
private const val DP_MEDIUM = 192
|
||||||
|
private const val DP_LARGE = 256
|
||||||
|
private const val DP_EXTRA_LARGE = 360
|
||||||
|
private const val REQUEST_CODE_NEW_TAB = 0
|
||||||
|
private const val REQUEST_CODE_VOICE = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SearchWidgetProviderSize {
|
||||||
|
EXTRA_SMALL,
|
||||||
|
SMALL,
|
||||||
|
MEDIUM,
|
||||||
|
LARGE,
|
||||||
|
EXTRA_LARGE
|
||||||
|
}
|
|
@ -65,6 +65,11 @@ class SearchFragment : Fragment(), BackHandler {
|
||||||
?.let { it.sessionId }
|
?.let { it.sessionId }
|
||||||
?.let(requireComponents.core.sessionManager::findSessionById)
|
?.let(requireComponents.core.sessionManager::findSessionById)
|
||||||
|
|
||||||
|
val displaySearchShortcuts = arguments
|
||||||
|
?.let(SearchFragmentArgs.Companion::fromBundle)
|
||||||
|
?.let { it.displaySearchShortcuts }
|
||||||
|
?: false
|
||||||
|
|
||||||
val view = inflater.inflate(R.layout.fragment_search, container, false)
|
val view = inflater.inflate(R.layout.fragment_search, container, false)
|
||||||
val url = session?.url ?: ""
|
val url = session?.url ?: ""
|
||||||
|
|
||||||
|
@ -72,7 +77,7 @@ class SearchFragment : Fragment(), BackHandler {
|
||||||
SearchStore(
|
SearchStore(
|
||||||
SearchState(
|
SearchState(
|
||||||
query = url,
|
query = url,
|
||||||
showShortcutEnginePicker = false,
|
showShortcutEnginePicker = displaySearchShortcuts,
|
||||||
searchEngineSource = SearchEngineSource.Default(
|
searchEngineSource = SearchEngineSource.Default(
|
||||||
requireComponents.search.searchEngineManager.getDefaultSearchEngine(requireContext())
|
requireComponents.search.searchEngineManager.getDefaultSearchEngine(requireContext())
|
||||||
),
|
),
|
||||||
|
@ -165,6 +170,8 @@ class SearchFragment : Fragment(), BackHandler {
|
||||||
} else {
|
} else {
|
||||||
requireComponents.analytics.metrics.track(Event.SearchShortcutMenuOpened)
|
requireComponents.analytics.metrics.track(Event.SearchShortcutMenuOpened)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchInteractor.turnOnStartedTyping()
|
||||||
}
|
}
|
||||||
|
|
||||||
consumeFrom(searchStore) {
|
consumeFrom(searchStore) {
|
||||||
|
|
|
@ -30,6 +30,10 @@ class SearchInteractor(
|
||||||
private val store: SearchStore
|
private val store: SearchStore
|
||||||
) : AwesomeBarInteractor, ToolbarInteractor {
|
) : AwesomeBarInteractor, ToolbarInteractor {
|
||||||
|
|
||||||
|
data class UserTypingCheck(var ranOnTextChanged: Boolean, var userHasTyped: Boolean)
|
||||||
|
|
||||||
|
private val userTypingCheck = UserTypingCheck(false, !store.state.showShortcutEnginePicker)
|
||||||
|
|
||||||
override fun onUrlCommitted(url: String) {
|
override fun onUrlCommitted(url: String) {
|
||||||
if (url.isNotBlank()) {
|
if (url.isNotBlank()) {
|
||||||
(context as HomeActivity).openToBrowserAndLoad(
|
(context as HomeActivity).openToBrowserAndLoad(
|
||||||
|
@ -55,6 +59,13 @@ class SearchInteractor(
|
||||||
|
|
||||||
override fun onTextChanged(text: String) {
|
override fun onTextChanged(text: String) {
|
||||||
store.dispatch(SearchAction.UpdateQuery(text))
|
store.dispatch(SearchAction.UpdateQuery(text))
|
||||||
|
|
||||||
|
if (userTypingCheck.ranOnTextChanged && !userTypingCheck.userHasTyped) {
|
||||||
|
store.dispatch(SearchAction.ShowSearchShortcutEnginePicker(false))
|
||||||
|
turnOnStartedTyping()
|
||||||
|
}
|
||||||
|
|
||||||
|
userTypingCheck.ranOnTextChanged = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUrlTapped(url: String) {
|
override fun onUrlTapped(url: String) {
|
||||||
|
@ -90,6 +101,11 @@ class SearchInteractor(
|
||||||
navController.navigate(directions)
|
navController.navigate(directions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun turnOnStartedTyping() {
|
||||||
|
userTypingCheck.ranOnTextChanged = true
|
||||||
|
userTypingCheck.userHasTyped = true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onExistingSessionSelected(session: Session) {
|
override fun onExistingSessionSelected(session: Session) {
|
||||||
val directions = SearchFragmentDirections.actionSearchFragmentToBrowserFragment(null)
|
val directions = SearchFragmentDirections.actionSearchFragmentToBrowserFragment(null)
|
||||||
navController.nav(R.id.searchFragment, directions)
|
navController.nav(R.id.searchFragment, directions)
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,8 @@
|
||||||
|
<!-- 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/. -->
|
||||||
|
|
||||||
|
<vector android:height="32dp" android:viewportHeight="24"
|
||||||
|
android:viewportWidth="24" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FF000000" android:fillType="evenOdd" android:pathData="M17,11v-1a1,1 0,0 1,2 0v1a7,7 0,0 1,-6 6.93L13,21a1,1 0,0 1,-2 0v-3.07A7,7 0,0 1,5 11v-1a1,1 0,0 1,2 0v1a5,5 0,0 0,10 0zM12,2a3,3 0,0 1,3 3v6a3,3 0,0 1,-6 0L9,5a3,3 0,0 1,3 -3z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/white_color" />
|
||||||
|
<corners android:radius="@dimen/tab_corner_radius"/>
|
||||||
|
</shape>
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<RelativeLayout android:layout_width="360dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:background="@drawable/rounded_white_corners"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
<LinearLayout android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="9dp"
|
||||||
|
android:id="@+id/button_search_widget_new_tab">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:background="@drawable/ic_logo_widget"
|
||||||
|
android:layout_marginLeft="9dp"
|
||||||
|
android:layout_marginRight="12dp"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_search_widget"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="15dp"
|
||||||
|
android:textColor="@color/search_widget_text"
|
||||||
|
android:letterSpacing="-0.025"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_search_widget_voice"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:background="@drawable/ic_microphone_widget"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignTop="@id/button_search_widget_new_tab"
|
||||||
|
android:layout_marginRight="9dp"/>
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout android:layout_width="64dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:background="@drawable/rounded_white_corners"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@id/button_search_widget_new_tab"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="@drawable/ic_logo_widget"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginTop="5dp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<RelativeLayout android:layout_width="256dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:background="@drawable/rounded_white_corners"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
<LinearLayout android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="9dp"
|
||||||
|
android:id="@+id/button_search_widget_new_tab">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:background="@drawable/ic_logo_widget"
|
||||||
|
android:layout_marginLeft="9dp"
|
||||||
|
android:layout_marginRight="12dp"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_search_widget"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="15dp"
|
||||||
|
android:textColor="@color/search_widget_text"
|
||||||
|
android:letterSpacing="-0.025"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_search_widget_voice"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:background="@drawable/ic_microphone_widget"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignTop="@id/button_search_widget_new_tab"
|
||||||
|
android:layout_marginRight="9dp"/>
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<RelativeLayout android:layout_width="192dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:background="@drawable/rounded_white_corners"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
<LinearLayout android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="9dp"
|
||||||
|
android:id="@+id/button_search_widget_new_tab">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:background="@drawable/ic_logo_widget"
|
||||||
|
android:layout_marginLeft="9dp"
|
||||||
|
android:layout_marginRight="12dp"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_search_widget"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="15dp"
|
||||||
|
android:textColor="@color/search_widget_text"
|
||||||
|
android:letterSpacing="-0.025"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_search_widget_voice"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:background="@drawable/ic_microphone_widget"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignTop="@id/button_search_widget_new_tab"
|
||||||
|
android:layout_marginRight="9dp"/>
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<RelativeLayout android:layout_width="100dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:background="@drawable/rounded_white_corners"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
<LinearLayout android:id="@+id/button_search_widget_new_tab"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="9dp">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:background="@drawable/ic_logo_widget"
|
||||||
|
android:layout_marginLeft="9dp"
|
||||||
|
android:layout_marginRight="12dp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_search_widget_voice"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:background="@drawable/ic_microphone_widget"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignTop="@id/button_search_widget_new_tab"
|
||||||
|
android:layout_marginRight="9dp"/>
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -11,6 +11,12 @@
|
||||||
app:popUpTo="@id/nav_graph"
|
app:popUpTo="@id/nav_graph"
|
||||||
app:popUpToInclusive="true" />
|
app:popUpToInclusive="true" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_global_search"
|
||||||
|
app:destination="@id/searchFragment"
|
||||||
|
app:popUpTo="@id/nav_graph"
|
||||||
|
app:popUpToInclusive="true"/>
|
||||||
|
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_global_crash_reporter"
|
android:id="@+id/action_global_crash_reporter"
|
||||||
app:destination="@id/crashReporterFragment" />
|
app:destination="@id/crashReporterFragment" />
|
||||||
|
@ -66,6 +72,11 @@
|
||||||
android:name="session_id"
|
android:name="session_id"
|
||||||
app:argType="string"
|
app:argType="string"
|
||||||
app:nullable="true" />
|
app:nullable="true" />
|
||||||
|
<argument
|
||||||
|
android:name="displaySearchShortcuts"
|
||||||
|
android:defaultValue="false"
|
||||||
|
app:argType="boolean"
|
||||||
|
app:nullable="false" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_searchFragment_to_searchEngineFragment"
|
android:id="@+id/action_searchFragment_to_searchEngineFragment"
|
||||||
app:destination="@id/searchEngineFragment"
|
app:destination="@id/searchEngineFragment"
|
||||||
|
|
|
@ -209,4 +209,7 @@
|
||||||
<color name="grey_button_color">#E0E0E6</color>
|
<color name="grey_button_color">#E0E0E6</color>
|
||||||
<color name="destructive_button_text_color">#C50042</color>
|
<color name="destructive_button_text_color">#C50042</color>
|
||||||
<color name="button_text_color">#312A65</color>
|
<color name="button_text_color">#312A65</color>
|
||||||
|
|
||||||
|
<!-- Search Widget -->
|
||||||
|
<color name="search_widget_text">#737373</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -81,6 +81,12 @@
|
||||||
<!-- Button in the search view that lets a user navigate to the site in their clipboard -->
|
<!-- Button in the search view that lets a user navigate to the site in their clipboard -->
|
||||||
<string name="awesomebar_clipboard_title">Fill link from clipboard</string>
|
<string name="awesomebar_clipboard_title">Fill link from clipboard</string>
|
||||||
|
|
||||||
|
<!-- Search Widget -->
|
||||||
|
<!-- Text preview for smaller sized widgets -->
|
||||||
|
<string name="search_widget_text_short">Search</string>
|
||||||
|
<!-- Text preview for larger sized widgets -->
|
||||||
|
<string name="search_widget_text_long">Search the web</string>
|
||||||
|
|
||||||
<!-- Preferences -->
|
<!-- Preferences -->
|
||||||
<!-- Title for the settings page-->
|
<!-- Title for the settings page-->
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:minWidth="256dp"
|
||||||
|
android:minHeight="50dp"
|
||||||
|
android:resizeMode="horizontal"
|
||||||
|
android:minResizeWidth="30dp"
|
||||||
|
android:previewImage="@drawable/fenix_search_widget"
|
||||||
|
android:updatePeriodMillis="86400000"
|
||||||
|
android:initialLayout="@layout/search_widget_large"
|
||||||
|
android:widgetCategory="home_screen">
|
||||||
|
</appwidget-provider>
|
|
@ -38,6 +38,7 @@ class SearchInteractorTest {
|
||||||
every { context.openToBrowserAndLoad(any(), any(), any(), any(), any(), any()) } just Runs
|
every { context.openToBrowserAndLoad(any(), any(), any(), any(), any(), any()) } just Runs
|
||||||
|
|
||||||
every { store.state } returns state
|
every { store.state } returns state
|
||||||
|
every { state.showShortcutEnginePicker } returns true
|
||||||
every { state.session } returns null
|
every { state.session } returns null
|
||||||
every { state.searchEngineSource } returns searchEngine
|
every { state.searchEngineSource } returns searchEngine
|
||||||
|
|
||||||
|
@ -58,7 +59,11 @@ class SearchInteractorTest {
|
||||||
@Test
|
@Test
|
||||||
fun onEditingCanceled() {
|
fun onEditingCanceled() {
|
||||||
val navController: NavController = mockk(relaxed = true)
|
val navController: NavController = mockk(relaxed = true)
|
||||||
val interactor = SearchInteractor(mockk(), navController, mockk())
|
val store: SearchStore = mockk()
|
||||||
|
|
||||||
|
every { store.state } returns mockk(relaxed = true)
|
||||||
|
|
||||||
|
val interactor = SearchInteractor(mockk(), navController, store)
|
||||||
|
|
||||||
interactor.onEditingCanceled()
|
interactor.onEditingCanceled()
|
||||||
|
|
||||||
|
@ -70,6 +75,9 @@ class SearchInteractorTest {
|
||||||
@Test
|
@Test
|
||||||
fun onTextChanged() {
|
fun onTextChanged() {
|
||||||
val store: SearchStore = mockk(relaxed = true)
|
val store: SearchStore = mockk(relaxed = true)
|
||||||
|
|
||||||
|
every { store.state } returns mockk(relaxed = true)
|
||||||
|
|
||||||
val interactor = SearchInteractor(mockk(), mockk(), store)
|
val interactor = SearchInteractor(mockk(), mockk(), store)
|
||||||
|
|
||||||
interactor.onTextChanged("test")
|
interactor.onTextChanged("test")
|
||||||
|
@ -88,6 +96,7 @@ class SearchInteractorTest {
|
||||||
|
|
||||||
every { store.state } returns state
|
every { store.state } returns state
|
||||||
every { state.session } returns null
|
every { state.session } returns null
|
||||||
|
every { state.showShortcutEnginePicker } returns true
|
||||||
|
|
||||||
val interactor = SearchInteractor(context, mockk(), store)
|
val interactor = SearchInteractor(context, mockk(), store)
|
||||||
|
|
||||||
|
@ -117,6 +126,7 @@ class SearchInteractorTest {
|
||||||
every { store.state } returns state
|
every { store.state } returns state
|
||||||
every { state.session } returns null
|
every { state.session } returns null
|
||||||
every { state.searchEngineSource } returns searchEngine
|
every { state.searchEngineSource } returns searchEngine
|
||||||
|
every { state.showShortcutEnginePicker } returns true
|
||||||
|
|
||||||
val interactor = SearchInteractor(context, mockk(), store)
|
val interactor = SearchInteractor(context, mockk(), store)
|
||||||
|
|
||||||
|
@ -137,6 +147,10 @@ class SearchInteractorTest {
|
||||||
every { context.metrics } returns mockk(relaxed = true)
|
every { context.metrics } returns mockk(relaxed = true)
|
||||||
|
|
||||||
val store: SearchStore = mockk(relaxed = true)
|
val store: SearchStore = mockk(relaxed = true)
|
||||||
|
val state: SearchState = mockk(relaxed = true)
|
||||||
|
|
||||||
|
every { store.state } returns state
|
||||||
|
|
||||||
val interactor = SearchInteractor(context, mockk(), store)
|
val interactor = SearchInteractor(context, mockk(), store)
|
||||||
val searchEngine: SearchEngine = mockk(relaxed = true)
|
val searchEngine: SearchEngine = mockk(relaxed = true)
|
||||||
|
|
||||||
|
@ -148,7 +162,11 @@ class SearchInteractorTest {
|
||||||
@Test
|
@Test
|
||||||
fun onClickSearchEngineSettings() {
|
fun onClickSearchEngineSettings() {
|
||||||
val navController: NavController = mockk()
|
val navController: NavController = mockk()
|
||||||
val interactor = SearchInteractor(mockk(), navController, mockk())
|
val store: SearchStore = mockk()
|
||||||
|
|
||||||
|
every { store.state } returns mockk(relaxed = true)
|
||||||
|
|
||||||
|
val interactor = SearchInteractor(mockk(), navController, store)
|
||||||
|
|
||||||
every { navController.navigate(any() as NavDirections) } just Runs
|
every { navController.navigate(any() as NavDirections) } just Runs
|
||||||
|
|
||||||
|
@ -166,7 +184,9 @@ class SearchInteractorTest {
|
||||||
val context: Context = mockk(relaxed = true)
|
val context: Context = mockk(relaxed = true)
|
||||||
val applicationContext: FenixApplication = mockk(relaxed = true)
|
val applicationContext: FenixApplication = mockk(relaxed = true)
|
||||||
every { context.applicationContext } returns applicationContext
|
every { context.applicationContext } returns applicationContext
|
||||||
val interactor = SearchInteractor(context, navController, mockk())
|
val store: SearchStore = mockk()
|
||||||
|
every { store.state } returns mockk(relaxed = true)
|
||||||
|
val interactor = SearchInteractor(context, navController, store)
|
||||||
val session = Session("http://mozilla.org", false)
|
val session = Session("http://mozilla.org", false)
|
||||||
|
|
||||||
interactor.onExistingSessionSelected(session)
|
interactor.onExistingSessionSelected(session)
|
||||||
|
|
Loading…
Reference in New Issue