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.

DefaultToolbarMenu.kt 16.3 KB
Newer Older
Sawyer Blatz's avatar
Sawyer Blatz committed
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/. */
Sawyer Blatz's avatar
Sawyer Blatz committed
4 5 6 7

package org.mozilla.fenix.components.toolbar

import android.content.Context
Tiger Oakes's avatar
Tiger Oakes committed
8
import androidx.annotation.ColorRes
9
import androidx.core.content.ContextCompat.getColor
Severin Rudie's avatar
Severin Rudie committed
10 11 12 13
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
14
import mozilla.components.browser.menu.BrowserMenuHighlight
Gabriel Luong's avatar
Gabriel Luong committed
15
import mozilla.components.browser.menu.WebExtensionBrowserMenuBuilder
Sawyer Blatz's avatar
Sawyer Blatz committed
16
import mozilla.components.browser.menu.item.BrowserMenuDivider
17
import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
18
import mozilla.components.browser.menu.item.BrowserMenuImageSwitch
Sawyer Blatz's avatar
Sawyer Blatz committed
19 20
import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.browser.menu.item.BrowserMenuItemToolbar
Severin Rudie's avatar
Severin Rudie committed
21 22
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
23 24
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.store.BrowserStore
Severin Rudie's avatar
Severin Rudie committed
25
import mozilla.components.concept.storage.BookmarksStorage
26
import mozilla.components.support.ktx.android.content.getColorFromAttr
27
import org.mozilla.fenix.HomeActivity
28
import org.mozilla.fenix.R
29
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
30
import org.mozilla.fenix.ext.asActivity
Sawyer Blatz's avatar
Sawyer Blatz committed
31
import org.mozilla.fenix.ext.components
32
import org.mozilla.fenix.ext.settings
33
import org.mozilla.fenix.theme.ThemeManager
Sawyer Blatz's avatar
Sawyer Blatz committed
34

Tiger Oakes's avatar
Tiger Oakes committed
35 36 37 38 39 40 41 42 43
/**
 * Builds the toolbar object used with the 3-dot menu in the browser fragment.
 * @param sessionManager Reference to the session manager that contains all tabs.
 * @param hasAccountProblem If true, there was a problem signing into the Firefox account.
 * @param shouldReverseItems If true, reverse the menu items.
 * @param onItemTapped Called when a menu item is tapped.
 * @param lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs.
 * @param bookmarksStorage Used to check if a page is bookmarked.
 */
44
@Suppress("LargeClass", "LongParameterList")
Sawyer Blatz's avatar
Sawyer Blatz committed
45 46
class DefaultToolbarMenu(
    private val context: Context,
Tiger Oakes's avatar
Tiger Oakes committed
47
    private val sessionManager: SessionManager,
48
    private val store: BrowserStore,
Tiger Oakes's avatar
Tiger Oakes committed
49 50
    hasAccountProblem: Boolean = false,
    shouldReverseItems: Boolean,
Severin Rudie's avatar
Severin Rudie committed
51 52
    private val onItemTapped: (ToolbarMenu.Item) -> Unit = {},
    private val lifecycleOwner: LifecycleOwner,
Tiger Oakes's avatar
Tiger Oakes committed
53
    private val bookmarksStorage: BookmarksStorage
Sawyer Blatz's avatar
Sawyer Blatz committed
54 55
) : ToolbarMenu {

Severin Rudie's avatar
Severin Rudie committed
56 57 58
    private var currentUrlIsBookmarked = false
    private var isBookmarkedJob: Job? = null

Tiger Oakes's avatar
Tiger Oakes committed
59 60 61
    /** Gets the current browser session */
    private val session: Session? get() = sessionManager.selectedSession

Gabriel Luong's avatar
Gabriel Luong committed
62 63 64
    override val menuBuilder by lazy {
        WebExtensionBrowserMenuBuilder(
            menuItems,
65
            endOfMenuAlwaysVisible = !shouldReverseItems,
66
            store = store,
67 68 69 70
            webExtIconTintColorResource = primaryTextColor(),
            onAddonsManagerTapped = {
                onItemTapped.invoke(ToolbarMenu.Item.AddonsManager)
            },
71
            appendExtensionSubMenuAtStart = !shouldReverseItems
Gabriel Luong's avatar
Gabriel Luong committed
72 73
        )
    }
Sawyer Blatz's avatar
Sawyer Blatz committed
74 75

    override val menuToolbar by lazy {
76 77 78 79 80 81 82 83 84 85 86 87 88 89
        val back = BrowserMenuItemToolbar.TwoStateButton(
            primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_back,
            primaryContentDescription = context.getString(R.string.browser_menu_back),
            primaryImageTintResource = primaryTextColor(),
            isInPrimaryState = {
                session?.canGoBack ?: true
            },
            secondaryImageTintResource = ThemeManager.resolveAttribute(R.attr.disabled, context),
            disableInSecondaryState = true,
            longClickListener = { onItemTapped.invoke(ToolbarMenu.Item.Back(viewHistory = true)) }
        ) {
            onItemTapped.invoke(ToolbarMenu.Item.Back(viewHistory = false))
        }

Sawyer Blatz's avatar
Sawyer Blatz committed
90 91 92
        val forward = BrowserMenuItemToolbar.TwoStateButton(
            primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_forward,
            primaryContentDescription = context.getString(R.string.browser_menu_forward),
Tiger Oakes's avatar
Tiger Oakes committed
93
            primaryImageTintResource = primaryTextColor(),
Sawyer Blatz's avatar
Sawyer Blatz committed
94
            isInPrimaryState = {
Tiger Oakes's avatar
Tiger Oakes committed
95
                session?.canGoForward ?: true
Sawyer Blatz's avatar
Sawyer Blatz committed
96
            },
Tiger Oakes's avatar
Tiger Oakes committed
97
            secondaryImageTintResource = ThemeManager.resolveAttribute(R.attr.disabled, context),
98 99
            disableInSecondaryState = true,
            longClickListener = { onItemTapped.invoke(ToolbarMenu.Item.Forward(viewHistory = true)) }
Sawyer Blatz's avatar
Sawyer Blatz committed
100
        ) {
101
            onItemTapped.invoke(ToolbarMenu.Item.Forward(viewHistory = false))
Sawyer Blatz's avatar
Sawyer Blatz committed
102 103 104 105 106
        }

        val refresh = BrowserMenuItemToolbar.TwoStateButton(
            primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_refresh,
            primaryContentDescription = context.getString(R.string.browser_menu_refresh),
Tiger Oakes's avatar
Tiger Oakes committed
107
            primaryImageTintResource = primaryTextColor(),
Sawyer Blatz's avatar
Sawyer Blatz committed
108
            isInPrimaryState = {
Tiger Oakes's avatar
Tiger Oakes committed
109
                session?.loading == false
Sawyer Blatz's avatar
Sawyer Blatz committed
110 111 112
            },
            secondaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_stop,
            secondaryContentDescription = context.getString(R.string.browser_menu_stop),
Tiger Oakes's avatar
Tiger Oakes committed
113
            secondaryImageTintResource = primaryTextColor(),
114 115
            disableInSecondaryState = false,
            longClickListener = { onItemTapped.invoke(ToolbarMenu.Item.Reload(bypassCache = true)) }
Sawyer Blatz's avatar
Sawyer Blatz committed
116
        ) {
Tiger Oakes's avatar
Tiger Oakes committed
117
            if (session?.loading == true) {
Sawyer Blatz's avatar
Sawyer Blatz committed
118 119
                onItemTapped.invoke(ToolbarMenu.Item.Stop)
            } else {
120
                onItemTapped.invoke(ToolbarMenu.Item.Reload(bypassCache = false))
Sawyer Blatz's avatar
Sawyer Blatz committed
121 122 123
            }
        }

Severin Rudie's avatar
Severin Rudie committed
124 125 126 127 128 129 130 131 132
        val share = BrowserMenuItemToolbar.Button(
            imageResource = R.drawable.mozac_ic_share,
            contentDescription = context.getString(R.string.browser_menu_share),
            iconTintColorResource = primaryTextColor(),
            listener = {
                onItemTapped.invoke(ToolbarMenu.Item.Share)
            }
        )

Tiger Oakes's avatar
Tiger Oakes committed
133
        registerForIsBookmarkedUpdates()
Severin Rudie's avatar
Severin Rudie committed
134 135 136
        val bookmark = BrowserMenuItemToolbar.TwoStateButton(
            primaryImageResource = R.drawable.ic_bookmark_filled,
            primaryContentDescription = context.getString(R.string.browser_menu_edit_bookmark),
Tiger Oakes's avatar
Tiger Oakes committed
137
            primaryImageTintResource = primaryTextColor(),
Severin Rudie's avatar
Severin Rudie committed
138 139 140 141 142 143
            // TwoStateButton.isInPrimaryState must be synchronous, and checking bookmark state is
            // relatively slow. The best we can do here is periodically compute and cache a new "is
            // bookmarked" state, and use that whenever the menu has been opened.
            isInPrimaryState = { currentUrlIsBookmarked },
            secondaryImageResource = R.drawable.ic_bookmark_outline,
            secondaryContentDescription = context.getString(R.string.browser_menu_bookmark),
Tiger Oakes's avatar
Tiger Oakes committed
144
            secondaryImageTintResource = primaryTextColor(),
Severin Rudie's avatar
Severin Rudie committed
145 146 147 148 149 150
            disableInSecondaryState = false
        ) {
            if (!currentUrlIsBookmarked) currentUrlIsBookmarked = true
            onItemTapped.invoke(ToolbarMenu.Item.Bookmark)
        }

151
        BrowserMenuItemToolbar(listOf(back, forward, bookmark, share, refresh))
Sawyer Blatz's avatar
Sawyer Blatz committed
152 153
    }

154
    // Predicates that need to be repeatedly called as the session changes
155 156 157 158 159 160 161
    private fun canAddToHomescreen(): Boolean =
        session != null && context.components.useCases.webAppUseCases.isPinningSupported() &&
                !context.components.useCases.webAppUseCases.isInstallable()

    private fun canInstall(): Boolean =
        session != null && context.components.useCases.webAppUseCases.isPinningSupported() &&
                context.components.useCases.webAppUseCases.isInstallable()
162 163 164 165 166 167

    private fun shouldShowOpenInApp(): Boolean = session?.let { session ->
        val appLink = context.components.useCases.appLinksUseCases.appLinkRedirect
        appLink(session.url).hasExternalApp()
    } ?: false

168 169 170
    private fun shouldShowReaderAppearance(): Boolean = session?.let {
        store.state.findTab(it.id)?.readerState?.active
    } ?: false
171 172
    // End of predicates //

Sawyer Blatz's avatar
Sawyer Blatz committed
173
    private val menuItems by lazy {
Severin Rudie's avatar
Severin Rudie committed
174
        // Predicates that are called once, during screen init
175 176
        val shouldShowSaveToCollection = (context.asActivity() as? HomeActivity)
            ?.browsingModeManager?.mode == BrowsingMode.Normal
177
        val shouldDeleteDataOnQuit = context.components.settings
178 179
            .shouldDeleteBrowsingDataOnQuit &&
            !context.components.settings.shouldDisableNormalMode
180 181
        val syncedTabsInTabsTray = context.components.settings
            .syncedTabsInTabsTray
Sawyer Blatz's avatar
Sawyer Blatz committed
182

183
        val menuItems = listOfNotNull(
184
            downloadsItem,
185 186
            historyItem,
            bookmarksItem,
187
            if (syncedTabsInTabsTray) null else syncedTabs,
188 189 190 191 192
            settings,
            if (shouldDeleteDataOnQuit) deleteDataOnQuit else null,
            BrowserMenuDivider(),
            findInPage,
            addToTopSites,
193 194
            addToHomescreen.apply { visible = ::canAddToHomescreen },
            installToHomescreen.apply { visible = ::canInstall },
Severin Rudie's avatar
Severin Rudie committed
195
            if (shouldShowSaveToCollection) saveToCollection else null,
196 197
            desktopMode,
            openInApp.apply { visible = ::shouldShowOpenInApp },
Severin Rudie's avatar
Severin Rudie committed
198 199 200 201
            readerAppearance.apply { visible = ::shouldShowReaderAppearance },
            BrowserMenuDivider(),
            menuToolbar
        )
202

203 204 205 206 207
        if (shouldReverseItems) {
            menuItems.reversed()
        } else {
            menuItems
        }
Severin Rudie's avatar
Severin Rudie committed
208
    }
Sawyer Blatz's avatar
Sawyer Blatz committed
209

Severin Rudie's avatar
Severin Rudie committed
210 211
    private val settings = BrowserMenuHighlightableItem(
        label = context.getString(R.string.browser_menu_settings),
212
        startImageResource = R.drawable.ic_settings,
Severin Rudie's avatar
Severin Rudie committed
213
        iconTintColorResource = if (hasAccountProblem)
214
            ThemeManager.resolveAttribute(R.attr.syncDisconnected, context) else
Severin Rudie's avatar
Severin Rudie committed
215 216
            primaryTextColor(),
        textColorResource = if (hasAccountProblem)
217
            ThemeManager.resolveAttribute(R.attr.primaryText, context) else
Severin Rudie's avatar
Severin Rudie committed
218
            primaryTextColor(),
219
        highlight = BrowserMenuHighlight.HighPriority(
220
            endImageResource = R.drawable.ic_sync_disconnected,
221 222
            backgroundTint = context.getColorFromAttr(R.attr.syncDisconnectedBackground),
            canPropagate = false
223 224
        ),
        isHighlighted = { hasAccountProblem }
Severin Rudie's avatar
Severin Rudie committed
225 226 227
    ) {
        onItemTapped.invoke(ToolbarMenu.Item.Settings)
    }
Sawyer Blatz's avatar
Sawyer Blatz committed
228

Severin Rudie's avatar
Severin Rudie committed
229 230 231
    private val desktopMode = BrowserMenuImageSwitch(
        imageResource = R.drawable.ic_desktop,
        label = context.getString(R.string.browser_menu_desktop_site),
Tiger Oakes's avatar
Tiger Oakes committed
232 233 234
        initialState = {
            session?.desktopMode ?: false
        }
Severin Rudie's avatar
Severin Rudie committed
235 236 237
    ) { checked ->
        onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked))
    }
Sawyer Blatz's avatar
Sawyer Blatz committed
238

239 240
    private val addToTopSites = BrowserMenuImageText(
        label = context.getString(R.string.browser_menu_add_to_top_sites),
241
        imageResource = R.drawable.ic_top_sites,
242 243
        iconTintColorResource = primaryTextColor()
    ) {
244
        onItemTapped.invoke(ToolbarMenu.Item.AddToTopSites)
245 246
    }

247
    private val addToHomescreen = BrowserMenuImageText(
Severin Rudie's avatar
Severin Rudie committed
248
        label = context.getString(R.string.browser_menu_add_to_homescreen),
249 250 251 252 253 254
        imageResource = R.drawable.ic_add_to_homescreen,
        iconTintColorResource = primaryTextColor()
    ) {
        onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen)
    }

255 256
    private val syncedTabs = BrowserMenuImageText(
        label = context.getString(R.string.synced_tabs),
257
        imageResource = R.drawable.ic_synced_tabs,
258 259 260 261 262
        iconTintColorResource = primaryTextColor()
    ) {
        onItemTapped.invoke(ToolbarMenu.Item.SyncedTabs)
    }

263 264
    private val installToHomescreen = BrowserMenuHighlightableItem(
        label = context.getString(R.string.browser_menu_install_on_homescreen),
265 266 267 268 269 270 271
        startImageResource = R.drawable.ic_add_to_homescreen,
        iconTintColorResource = primaryTextColor(),
        highlight = BrowserMenuHighlight.LowPriority(
            label = context.getString(R.string.browser_menu_install_on_homescreen),
            notificationTint = getColor(context, R.color.whats_new_notification_color)
        ),
        isHighlighted = {
272
            !context.settings().installPwaOpened
273
        }
Severin Rudie's avatar
Severin Rudie committed
274
    ) {
275
        onItemTapped.invoke(ToolbarMenu.Item.InstallToHomeScreen)
Severin Rudie's avatar
Severin Rudie committed
276
    }
Sawyer Blatz's avatar
Sawyer Blatz committed
277

Severin Rudie's avatar
Severin Rudie committed
278 279 280 281 282 283 284
    private val findInPage = BrowserMenuImageText(
        label = context.getString(R.string.browser_menu_find_in_page),
        imageResource = R.drawable.mozac_ic_search,
        iconTintColorResource = primaryTextColor()
    ) {
        onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
    }
Sawyer Blatz's avatar
Sawyer Blatz committed
285

Severin Rudie's avatar
Severin Rudie committed
286
    private val saveToCollection = BrowserMenuImageText(
287
        label = context.getString(R.string.browser_menu_save_to_collection_2),
Severin Rudie's avatar
Severin Rudie committed
288 289 290 291 292
        imageResource = R.drawable.ic_tab_collection,
        iconTintColorResource = primaryTextColor()
    ) {
        onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection)
    }
Sawyer Blatz's avatar
Sawyer Blatz committed
293

Severin Rudie's avatar
Severin Rudie committed
294 295 296 297 298 299 300
    private val deleteDataOnQuit = BrowserMenuImageText(
        label = context.getString(R.string.delete_browsing_data_on_quit_action),
        imageResource = R.drawable.ic_exit,
        iconTintColorResource = primaryTextColor()
    ) {
        onItemTapped.invoke(ToolbarMenu.Item.Quit)
    }
301

Severin Rudie's avatar
Severin Rudie committed
302 303 304 305 306 307 308 309
    private val readerAppearance = BrowserMenuImageText(
        label = context.getString(R.string.browser_menu_read_appearance),
        imageResource = R.drawable.ic_readermode_appearance,
        iconTintColorResource = primaryTextColor()
    ) {
        onItemTapped.invoke(ToolbarMenu.Item.ReaderModeAppearance)
    }

310
    private val openInApp = BrowserMenuHighlightableItem(
Severin Rudie's avatar
Severin Rudie committed
311
        label = context.getString(R.string.browser_menu_open_app_link),
312
        startImageResource = R.drawable.ic_open_in_app,
313 314 315 316 317
        iconTintColorResource = primaryTextColor(),
        highlight = BrowserMenuHighlight.LowPriority(
            label = context.getString(R.string.browser_menu_open_app_link),
            notificationTint = getColor(context, R.color.whats_new_notification_color)
        ),
318
        isHighlighted = { !context.settings().openInAppOpened }
Tiger Oakes's avatar
Tiger Oakes committed
319
    ) {
Severin Rudie's avatar
Severin Rudie committed
320 321 322
        onItemTapped.invoke(ToolbarMenu.Item.OpenInApp)
    }

323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
    val historyItem = BrowserMenuImageText(
        context.getString(R.string.library_history),
        R.drawable.ic_history,
        primaryTextColor()
    ) {
        onItemTapped.invoke(ToolbarMenu.Item.History)
    }

    val bookmarksItem = BrowserMenuImageText(
        context.getString(R.string.library_bookmarks),
        R.drawable.ic_bookmark_filled,
        primaryTextColor()
    ) {
        onItemTapped.invoke(ToolbarMenu.Item.Bookmarks)
    }

Kate Glazko's avatar
Kate Glazko committed
339 340 341 342 343 344 345 346
    val downloadsItem = BrowserMenuImageText(
        "Downloads",
        R.drawable.ic_download,
        primaryTextColor()
    ) {
        onItemTapped.invoke(ToolbarMenu.Item.Downloads)
    }

Tiger Oakes's avatar
Tiger Oakes committed
347
    @ColorRes
Severin Rudie's avatar
Severin Rudie committed
348 349
    private fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context)

350 351
    private var currentSessionObserver: Pair<Session, Session.Observer>? = null

Tiger Oakes's avatar
Tiger Oakes committed
352
    private fun registerForIsBookmarkedUpdates() {
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
        session?.let {
            registerForUrlChanges(it)
        }

        val sessionManagerObserver = object : SessionManager.Observer {
            override fun onSessionSelected(session: Session) {
                // Unregister any old session observer before registering a new session observer
                currentSessionObserver?.let {
                    it.first.unregister(it.second)
                }
                currentUrlIsBookmarked = false
                updateCurrentUrlIsBookmarked(session.url)
                registerForUrlChanges(session)
            }
        }

        sessionManager.register(sessionManagerObserver, lifecycleOwner)
    }

    private fun registerForUrlChanges(session: Session) {
        val sessionObserver = object : Session.Observer {
Severin Rudie's avatar
Severin Rudie committed
374 375 376 377 378 379
            override fun onUrlChanged(session: Session, url: String) {
                currentUrlIsBookmarked = false
                updateCurrentUrlIsBookmarked(url)
            }
        }

380 381 382
        currentSessionObserver = Pair(session, sessionObserver)
        updateCurrentUrlIsBookmarked(session.url)
        session.register(sessionObserver, lifecycleOwner)
Severin Rudie's avatar
Severin Rudie committed
383 384 385 386 387 388 389 390 391
    }

    private fun updateCurrentUrlIsBookmarked(newUrl: String) {
        isBookmarkedJob?.cancel()
        isBookmarkedJob = lifecycleOwner.lifecycleScope.launch {
            currentUrlIsBookmarked = bookmarksStorage
                .getBookmarksWithUrl(newUrl)
                .any { it.url == newUrl }
        }
392
    }
Sawyer Blatz's avatar
Sawyer Blatz committed
393
}