HomeMenu.kt 9.74 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
6
7
package org.mozilla.fenix.home

import android.content.Context
8
import androidx.core.content.ContextCompat.getColor
9
10
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
11
12
13
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
14
import mozilla.components.browser.menu.BrowserMenuBuilder
15
import mozilla.components.browser.menu.BrowserMenuHighlight
16
import mozilla.components.browser.menu.ext.getHighlight
17
import mozilla.components.browser.menu.item.BrowserMenuDivider
18
import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
19
import mozilla.components.browser.menu.item.BrowserMenuImageText
20
21
22
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
23
import mozilla.components.support.ktx.android.content.getColorFromAttr
24
import org.mozilla.fenix.R
25
26
import org.mozilla.fenix.experiments.ExperimentBranch
import org.mozilla.fenix.experiments.Experiments
27
import org.mozilla.fenix.ext.components
28
import org.mozilla.fenix.ext.settings
29
import org.mozilla.fenix.ext.withExperiment
30
import org.mozilla.fenix.theme.ThemeManager
31
import org.mozilla.fenix.whatsnew.WhatsNew
32
33

class HomeMenu(
34
    private val lifecycleOwner: LifecycleOwner,
35
    private val context: Context,
36
37
38
    private val onItemTapped: (Item) -> Unit = {},
    private val onMenuBuilderChanged: (BrowserMenuBuilder) -> Unit = {},
    private val onHighlightPresent: (BrowserMenuHighlight) -> Unit = {}
39
40
) {
    sealed class Item {
41
        object WhatsNew : Item()
42
        object Help : Item()
43
        object AddonsManager : Item()
44
        object Settings : Item()
45
        object SyncedTabs : Item()
46
47
        object History : Item()
        object Bookmarks : Item()
Kate Glazko's avatar
Kate Glazko committed
48
        object Downloads : Item()
49
        object Quit : Item()
50
        object Sync : Item()
51
52
    }

53
54
    private val primaryTextColor =
        ThemeManager.resolveAttribute(R.attr.primaryText, context)
55
56
    private val syncDisconnectedColor = ThemeManager.resolveAttribute(R.attr.syncDisconnected, context)
    private val syncDisconnectedBackgroundColor = context.getColorFromAttr(R.attr.syncDisconnectedBackground)
57
58
59

    private val menuCategoryTextColor =
        ThemeManager.resolveAttribute(R.attr.menuCategoryText, context)
60
    private val shouldUseBottomToolbar = context.settings().shouldUseBottomToolbar
61

62
63
64
65
66
67
68
69
    // 'Reconnect' and 'Quit' items aren't needed most of the time, so we'll only create the if necessary.
    private val reconnectToSyncItem by lazy {
        BrowserMenuHighlightableItem(
            context.getString(R.string.sync_reconnect),
            R.drawable.ic_sync_disconnected,
            iconTintColorResource = syncDisconnectedColor,
            textColorResource = primaryTextColor,
            highlight = BrowserMenuHighlight.HighPriority(
70
71
                backgroundTint = syncDisconnectedBackgroundColor,
                canPropagate = false
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
            ),
            isHighlighted = { true }
        ) {
            onItemTapped.invoke(Item.Sync)
        }
    }

    private val quitItem by lazy {
        BrowserMenuImageText(
            context.getString(R.string.delete_browsing_data_on_quit_action),
            R.drawable.ic_exit,
            primaryTextColor
        ) {
            onItemTapped.invoke(Item.Quit)
        }
    }

89
    private val coreMenuItems by lazy {
90
91
92
93
94
95
96
97
98
99
100
        val whatsNewItem = BrowserMenuHighlightableItem(
            context.getString(R.string.browser_menu_whats_new),
            R.drawable.ic_whats_new,
            iconTintColorResource = primaryTextColor,
            highlight = BrowserMenuHighlight.LowPriority(
                notificationTint = getColor(context, R.color.whats_new_notification_color)
            ),
            isHighlighted = { WhatsNew.shouldHighlightWhatsNew(context) }
        ) {
            onItemTapped.invoke(Item.WhatsNew)
        }
101

102
        val experiments = context.components.analytics.experiments
103
104
105
106
        val bookmarksIcon = experiments.withExperiment(Experiments.BOOKMARK_ICON) {
            when (it) {
                ExperimentBranch.TREATMENT -> R.drawable.ic_bookmark_list
                else -> R.drawable.ic_bookmark_filled
107
            }
108
        }
109
110
        val bookmarksItem = BrowserMenuImageText(
            context.getString(R.string.library_bookmarks),
111
            bookmarksIcon,
112
113
114
115
            primaryTextColor
        ) {
            onItemTapped.invoke(Item.Bookmarks)
        }
116

117
118
119
120
121
122
123
        // We want to validate that the Nimbus experiments library is working, from the android UI
        // all the way back to the data science backend. We're not testing the user's preference
        // or response, we're end-to-end testing the experiments platform.
        // So here, we're running multiple identical branches with the same treatment, and if the
        // user isn't targeted, then we get still get the same treatment.
        // The `let` block is degenerate here, but left here so as to document the form of how experiments
        // are implemented here.
124
125
126
127
128
        val historyIcon = experiments.withExperiment(Experiments.A_A_NIMBUS_VALIDATION) {
            when (it) {
                ExperimentBranch.A1 -> R.drawable.ic_history
                ExperimentBranch.A2 -> R.drawable.ic_history
                else -> R.drawable.ic_history
129
            }
130
        }
131
        val historyItem = BrowserMenuImageText(
132
            context.getString(R.string.library_history),
133
            historyIcon,
134
135
            primaryTextColor
        ) {
136
137
            onItemTapped.invoke(Item.History)
        }
138

139
140
        val addons = BrowserMenuImageText(
            context.getString(R.string.browser_menu_add_ons),
141
            R.drawable.ic_addons_extensions,
142
143
144
145
146
            primaryTextColor
        ) {
            onItemTapped.invoke(Item.AddonsManager)
        }

147
148
149
150
151
152
153
154
        val settingsItem = BrowserMenuImageText(
            context.getString(R.string.browser_menu_settings),
            R.drawable.ic_settings,
            primaryTextColor
        ) {
            onItemTapped.invoke(Item.Settings)
        }

155
156
        val syncedTabsItem = BrowserMenuImageText(
            context.getString(R.string.library_synced_tabs),
157
            R.drawable.ic_synced_tabs,
158
159
160
161
162
            primaryTextColor
        ) {
            onItemTapped.invoke(Item.SyncedTabs)
        }

163
164
165
166
167
168
169
        val helpItem = BrowserMenuImageText(
            context.getString(R.string.browser_menu_help),
            R.drawable.ic_help,
            primaryTextColor
        ) {
            onItemTapped.invoke(Item.Help)
        }
170

Kate Glazko's avatar
Kate Glazko committed
171
        val downloadsItem = BrowserMenuImageText(
172
            context.getString(R.string.library_downloads),
Kate Glazko's avatar
Kate Glazko committed
173
174
175
176
177
178
            R.drawable.ic_download,
            primaryTextColor
        ) {
            onItemTapped.invoke(Item.Downloads)
        }

179
180
181
182
183
184
185
186
        // Only query account manager if it has been initialized.
        // We don't want to cause its initialization just for this check.
        val accountAuthItem = if (context.components.backgroundServices.accountManagerAvailableQueue.isReady()) {
            if (context.components.backgroundServices.accountManager.accountNeedsReauth()) reconnectToSyncItem else null
        } else {
            null
        }

187
188
189
190
191
192
        val settings = context.components.settings

        val menuItems = listOfNotNull(
            if (settings.shouldDeleteBrowsingDataOnQuit) quitItem else null,
            settingsItem,
            BrowserMenuDivider(),
193
            syncedTabsItem,
194
195
            bookmarksItem,
            historyItem,
196
            downloadsItem,
197
198
199
200
201
202
203
204
205
206
            BrowserMenuDivider(),
            addons,
            BrowserMenuDivider(),
            whatsNewItem,
            helpItem,
            accountAuthItem
        ).also { items ->
            items.getHighlight()?.let { onHighlightPresent(it) }
        }

207
        if (shouldUseBottomToolbar) {
208
            menuItems.reversed()
209
        } else {
210
            menuItems
211
212
        }
    }
213

214
215
216
217
218
219
220
221
222
223
224
225
    init {
        // Report initial state.
        onMenuBuilderChanged(BrowserMenuBuilder(coreMenuItems))

        // Observe account state changes, and update menu item builder with a new set of items.
        context.components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
            // This task isn't relevant if our parent fragment isn't around anymore.
            if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
                return@runIfReadyOrQueue
            }
            context.components.backgroundServices.accountManager.register(object : AccountObserver {
                override fun onAuthenticationProblems() {
226
227
228
229
230
                    lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
                        onMenuBuilderChanged(BrowserMenuBuilder(
                            listOf(reconnectToSyncItem) + coreMenuItems
                        ))
                    }
231
232
233
                }

                override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
234
235
236
237
238
239
240
                    lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
                        onMenuBuilderChanged(
                            BrowserMenuBuilder(
                                coreMenuItems
                            )
                        )
                    }
241
242
243
                }

                override fun onLoggedOut() {
244
245
246
247
248
249
250
                    lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
                        onMenuBuilderChanged(
                            BrowserMenuBuilder(
                                coreMenuItems
                            )
                        )
                    }
251
252
253
254
                }
            }, lifecycleOwner)
        }
    }
255
}