GitLab is used only for code review, issue tracking and project management. Canonical locations for source code are still https://gitweb.torproject.org/ https://git.torproject.org/ and git-rw.torproject.org.

SearchEngineListPreference.kt 9.59 KB
Newer Older
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2 3
 * 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/. */
4

5
package org.mozilla.fenix.settings.search
6 7 8 9 10 11

import android.content.Context
import android.content.res.Resources
import android.graphics.drawable.BitmapDrawable
import android.util.AttributeSet
import android.view.LayoutInflater
12
import android.view.View
13 14
import android.view.ViewGroup
import android.widget.CompoundButton
15
import android.widget.LinearLayout
16
import android.widget.RadioGroup
Jeff Boek's avatar
Jeff Boek committed
17 18
import androidx.core.view.isVisible
import androidx.navigation.Navigation
19 20
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
21 22 23 24
import kotlinx.android.synthetic.main.search_engine_radio_button.view.engine_icon
import kotlinx.android.synthetic.main.search_engine_radio_button.view.engine_text
import kotlinx.android.synthetic.main.search_engine_radio_button.view.overflow_menu
import kotlinx.android.synthetic.main.search_engine_radio_button.view.radio_button
Jeff Boek's avatar
Jeff Boek committed
25
import kotlinx.coroutines.MainScope
26
import mozilla.components.browser.search.SearchEngine
Jeff Boek's avatar
Jeff Boek committed
27
import mozilla.components.browser.search.provider.SearchEngineList
28
import org.mozilla.fenix.R
29
import org.mozilla.fenix.components.metrics.Event
Jeff Boek's avatar
Jeff Boek committed
30
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
31
import org.mozilla.fenix.ext.components
Jeff Boek's avatar
Jeff Boek committed
32
import org.mozilla.fenix.ext.getRootView
33
import org.mozilla.fenix.ext.settings
Jeff Boek's avatar
Jeff Boek committed
34
import org.mozilla.fenix.utils.allowUndo
35

36 37 38
abstract class SearchEngineListPreference @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
39
    defStyleAttr: Int = android.R.attr.preferenceStyle
40 41
) : Preference(context, attrs, defStyleAttr), CompoundButton.OnCheckedChangeListener {

Jeff Boek's avatar
Jeff Boek committed
42
    protected lateinit var searchEngineList: SearchEngineList
43 44 45 46
    protected var searchEngineGroup: RadioGroup? = null

    protected abstract val itemResId: Int

47
    init {
48 49 50 51 52 53
        layoutResource = R.layout.preference_search_engine_chooser
    }

    override fun onBindViewHolder(holder: PreferenceViewHolder?) {
        super.onBindViewHolder(holder)
        searchEngineGroup = holder!!.itemView.findViewById(R.id.search_engine_group)
Jeff Boek's avatar
Jeff Boek committed
54 55
        reload(searchEngineGroup!!.context)
    }
56

Jeff Boek's avatar
Jeff Boek committed
57 58
    fun reload(context: Context) {
        searchEngineList = context.components.search.provider.installedSearchEngines(context)
59 60 61
        refreshSearchEngineViews(context)
    }

62
    protected abstract fun onSearchEngineSelected(searchEngine: SearchEngine)
63 64 65 66 67 68 69 70 71 72
    protected abstract fun updateDefaultItem(defaultButton: CompoundButton)

    private fun refreshSearchEngineViews(context: Context) {
        if (searchEngineGroup == null) {
            // We want to refresh the search engine list of this preference in onResume,
            // but the first time this preference is created onResume is called before onCreateView
            // so searchEngineGroup is not set yet.
            return
        }

73 74
        val defaultEngineId = context.components.search.provider.getDefaultEngine(context).identifier

Jeff Boek's avatar
Jeff Boek committed
75
        val selectedEngine = (searchEngineList.list.find {
76
            it.identifier == defaultEngineId
Jeff Boek's avatar
Jeff Boek committed
77
        } ?: searchEngineList.list.first()).identifier
78

79 80
        // set the search engine manager default
        context.components.search.provider.setDefaultEngine(context, selectedEngine)
81

82 83 84 85 86 87 88 89
        searchEngineGroup!!.removeAllViews()

        val layoutInflater = LayoutInflater.from(context)
        val layoutParams = ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.WRAP_CONTENT
        )

90
        val setupSearchEngineItem: (Int, SearchEngine) -> Unit = { index, engine ->
91
            val engineId = engine.identifier
Jeff Boek's avatar
Jeff Boek committed
92 93 94 95 96 97 98 99
            val engineItem = makeButtonFromSearchEngine(
                engine = engine,
                layoutInflater = layoutInflater,
                res = context.resources,
                allowDeletion = searchEngineList.list.size > 1
            )

            engineItem.id = index + (searchEngineList.default?.let { 1 } ?: 0)
100
            engineItem.tag = engineId
Jeff Boek's avatar
Jeff Boek committed
101
            if (engineId == selectedEngine) {
102
                updateDefaultItem(engineItem.radio_button)
103 104 105 106
                /* #11465 -> radio_button.isChecked = true does not trigger
                * onSearchEngineSelected because searchEngineGroup has null views at that point.
                * So we trigger it here.*/
                onSearchEngineSelected(engine)
107 108 109
            }
            searchEngineGroup!!.addView(engineItem, layoutParams)
        }
110

Jeff Boek's avatar
Jeff Boek committed
111 112 113
        searchEngineList.default?.apply {
            setupSearchEngineItem(0, this)
        }
114

Jeff Boek's avatar
Jeff Boek committed
115 116
        searchEngineList.list
            .filter { it.identifier != searchEngineList.default?.identifier }
117
            .forEachIndexed(setupSearchEngineItem)
118 119 120 121 122
    }

    private fun makeButtonFromSearchEngine(
        engine: SearchEngine,
        layoutInflater: LayoutInflater,
Jeff Boek's avatar
Jeff Boek committed
123 124
        res: Resources,
        allowDeletion: Boolean
125
    ): View {
126 127
        val isCustomSearchEngine =
            CustomSearchEngineStore.isCustomSearchEngine(context, engine.identifier)
Jeff Boek's avatar
Jeff Boek committed
128

129
        val wrapper = layoutInflater.inflate(itemResId, null) as LinearLayout
130 131 132
        wrapper.setOnClickListener { wrapper.radio_button.isChecked = true }
        wrapper.radio_button.setOnCheckedChangeListener(this)
        wrapper.engine_text.text = engine.name
Jeff Boek's avatar
Jeff Boek committed
133 134 135 136 137 138 139 140 141
        wrapper.overflow_menu.isVisible = allowDeletion || isCustomSearchEngine
        wrapper.overflow_menu.setOnClickListener {
            SearchEngineMenu(
                context = context,
                allowDeletion = allowDeletion,
                isCustomSearchEngine = isCustomSearchEngine,
                onItemTapped = {
                    when (it) {
                        is SearchEngineMenu.Item.Edit -> editCustomSearchEngine(engine)
142 143 144 145 146
                        is SearchEngineMenu.Item.Delete -> deleteSearchEngine(
                            context,
                            engine,
                            isCustomSearchEngine
                        )
Jeff Boek's avatar
Jeff Boek committed
147 148 149 150
                    }
                }
            ).menuBuilder.build(context).show(wrapper.overflow_menu)
        }
151 152 153
        val iconSize = res.getDimension(R.dimen.preference_icon_drawable_size).toInt()
        val engineIcon = BitmapDrawable(res, engine.icon)
        engineIcon.setBounds(0, 0, iconSize, iconSize)
154 155 156 157 158
        wrapper.engine_icon.setImageDrawable(engineIcon)
        return wrapper
    }

    override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
Jeff Boek's avatar
Jeff Boek committed
159
        searchEngineList.list.forEach { engine ->
160 161
            val wrapper: LinearLayout =
                searchEngineGroup?.findViewWithTag(engine.identifier) ?: return
162 163 164 165 166 167 168 169 170 171

            when (wrapper.radio_button == buttonView) {
                true -> onSearchEngineSelected(engine)
                false -> {
                    wrapper.radio_button.setOnCheckedChangeListener(null)
                    wrapper.radio_button.isChecked = false
                    wrapper.radio_button.setOnCheckedChangeListener(this)
                }
            }
        }
172
    }
173

Jeff Boek's avatar
Jeff Boek committed
174
    private fun editCustomSearchEngine(engine: SearchEngine) {
175
        val wasDefault = context.components.search.provider.getDefaultEngine(context).identifier == engine.identifier
Jeff Boek's avatar
Jeff Boek committed
176
        val directions = SearchEngineFragmentDirections
177
            .actionSearchEngineFragmentToEditCustomSearchEngineFragment(engine.identifier, wasDefault)
Jeff Boek's avatar
Jeff Boek committed
178 179 180
        Navigation.findNavController(searchEngineGroup!!).navigate(directions)
    }

181 182 183 184 185
    private fun deleteSearchEngine(
        context: Context,
        engine: SearchEngine,
        isCustomSearchEngine: Boolean
    ) {
186 187 188 189
        val isDefaultEngine = engine == context.components.search.provider.getDefaultEngine(context)
        val initialEngineList = searchEngineList.copy()
        val initialDefaultEngine = searchEngineList.default

190 191 192 193 194
        context.components.search.provider.uninstallSearchEngine(
            context,
            engine,
            isCustomSearchEngine
        )
195

Jeff Boek's avatar
Jeff Boek committed
196 197 198 199 200 201
        MainScope().allowUndo(
            view = context.getRootView()!!,
            message = context
                .getString(R.string.search_delete_search_engine_success_message, engine.name),
            undoActionTitle = context.getString(R.string.snackbar_deleted_undo),
            onCancel = {
202 203 204 205 206
                context.components.search.provider.installSearchEngine(
                    context,
                    engine,
                    isCustomSearchEngine
                )
207 208 209 210

                searchEngineList = initialEngineList.copy(
                    default = initialDefaultEngine
                )
Jeff Boek's avatar
Jeff Boek committed
211 212 213 214

                refreshSearchEngineViews(context)
            },
            operation = {
215
                if (isDefaultEngine) {
216 217 218
                    val default = context.components.search.provider.getDefaultEngine(context)
                    context.components.search.provider.setDefaultEngine(context, default.identifier)
                    context.settings().defaultSearchEngineName = default.name
Jeff Boek's avatar
Jeff Boek committed
219
                }
220
                if (isCustomSearchEngine) {
221 222 223
                    context.components.analytics.metrics.track(Event.CustomEngineDeleted)
                }
                refreshSearchEngineViews(context)
Jeff Boek's avatar
Jeff Boek committed
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
            }
        )

        searchEngineList = searchEngineList.copy(
            list = searchEngineList.list.filter {
                it.identifier != engine.identifier
            },
            default = if (searchEngineList.default?.identifier == engine.identifier) {
                null
            } else {
                searchEngineList.default
            }
        )

        refreshSearchEngineViews(context)
239
    }
240
}