SessionControlController.kt 20.4 KB
Newer Older
1
2
3
4
5
6
/* 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.home.sessioncontrol

7
8
import android.view.LayoutInflater
import android.widget.EditText
9
import androidx.annotation.VisibleForTesting
10
import androidx.appcompat.app.AlertDialog
11
12
13
14
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
15
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
16
import mozilla.components.browser.state.state.availableSearchEngines
17
import mozilla.components.browser.state.state.searchEngines
18
19
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore
20
import mozilla.components.concept.engine.Engine
21
import mozilla.components.concept.engine.prompt.ShareData
22
import mozilla.components.feature.session.SessionUseCases
23
import mozilla.components.feature.tab.collections.TabCollection
24
import mozilla.components.feature.tab.collections.ext.invoke
25
import mozilla.components.feature.tabs.TabsUseCases
26
import mozilla.components.feature.top.sites.TopSite
27
import mozilla.components.support.ktx.android.view.showKeyboard
28
import mozilla.components.support.ktx.kotlin.isUrl
29
30
31
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
32
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
33
34
35
36
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
37
import org.mozilla.fenix.components.metrics.MetricsUtils
38
import org.mozilla.fenix.components.tips.Tip
39
40
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
41
42
43
44
45
46
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.settings.SupportUtils
47
import org.mozilla.fenix.utils.Settings
ekager's avatar
ekager committed
48
import mozilla.components.feature.tab.collections.Tab as ComponentTab
49
50
51
52
53

/**
 * [HomeFragment] controller. An interface that handles the view manipulation of the Tabs triggered
 * by the Interactor.
 */
54
@Suppress("TooManyFunctions")
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
interface SessionControlController {
    /**
     * @see [CollectionInteractor.onCollectionAddTabTapped]
     */
    fun handleCollectionAddTabTapped(collection: TabCollection)

    /**
     * @see [CollectionInteractor.onCollectionOpenTabClicked]
     */
    fun handleCollectionOpenTabClicked(tab: ComponentTab)

    /**
     * @see [CollectionInteractor.onCollectionOpenTabsTapped]
     */
    fun handleCollectionOpenTabsTapped(collection: TabCollection)

    /**
     * @see [CollectionInteractor.onCollectionRemoveTab]
     */
74
    fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab, wasSwiped: Boolean)
75
76
77
78
79
80
81
82
83
84
85

    /**
     * @see [CollectionInteractor.onCollectionShareTabsClicked]
     */
    fun handleCollectionShareTabsClicked(collection: TabCollection)

    /**
     * @see [CollectionInteractor.onDeleteCollectionTapped]
     */
    fun handleDeleteCollectionTapped(collection: TabCollection)

86
87
88
89
90
    /**
     * @see [TopSiteInteractor.onOpenInPrivateTabClicked]
     */
    fun handleOpenInPrivateTabClicked(topSite: TopSite)

91
92
93
94
95
    /**
     * @see [TabSessionInteractor.onPrivateBrowsingLearnMoreClicked]
     */
    fun handlePrivateBrowsingLearnMoreClicked()

96
97
98
99
100
    /**
     * @see [TopSiteInteractor.onRenameTopSiteClicked]
     */
    fun handleRenameTopSiteClicked(topSite: TopSite)

101
102
103
104
105
    /**
     * @see [TopSiteInteractor.onRemoveTopSiteClicked]
     */
    fun handleRemoveTopSiteClicked(topSite: TopSite)

106
107
108
109
110
    /**
     * @see [CollectionInteractor.onRenameCollectionTapped]
     */
    fun handleRenameCollectionTapped(collection: TabCollection)

111
112
113
    /**
     * @see [TopSiteInteractor.onSelectTopSite]
     */
114
    fun handleSelectTopSite(url: String, type: TopSite.Type)
115

116
117
118
119
120
    /**
     * @see [OnboardingInteractor.onStartBrowsingClicked]
     */
    fun handleStartBrowsingClicked()

121
122
123
124
125
    /**
     * @see [OnboardingInteractor.onOpenSettingsClicked]
     */
    fun handleOpenSettingsClicked()

Matthew Finkel's avatar
Matthew Finkel committed
126
127
128
129
130
    /**
     * @see [OnboardingInteractor.onOpenSecurityLevelSettingsClicked]
     */
    fun handleOpenSecurityLevelSettingsClicked()

131
132
133
134
135
136
137
138
139
140
    /**
     * @see [OnboardingInteractor.onWhatsNewGetAnswersClicked]
     */
    fun handleWhatsNewGetAnswersClicked()

    /**
     * @see [OnboardingInteractor.onReadPrivacyNoticeClicked]
     */
    fun handleReadPrivacyNoticeClicked()

Matthew Finkel's avatar
Matthew Finkel committed
141
142
143
144
145
    /**
     * @see [OnboardingInteractor.onDonateClicked]
     */
    fun handleDonateClicked()

146
147
148
149
    /**
     * @see [CollectionInteractor.onToggleCollectionExpanded]
     */
    fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean)
150

151
152
153
    /**
     * @see [TipInteractor.onCloseTip]
     */
154
    fun handleCloseTip(tip: Tip)
155

156
157
158
159
160
161
162
163
164
165
    /**
     * @see [ToolbarInteractor.onPasteAndGo]
     */
    fun handlePasteAndGo(clipboardText: String)

    /**
     * @see [ToolbarInteractor.onPaste]
     */
    fun handlePaste(clipboardText: String)

166
167
168
169
    /**
     * @see [CollectionInteractor.onAddTabsToCollectionTapped]
     */
    fun handleCreateCollection()
170
171
172
173
174

    /**
     * @see [CollectionInteractor.onRemoveCollectionsPlaceholder]
     */
    fun handleRemoveCollectionsPlaceholder()
175
176
177
178
179

    /**
     * @see [CollectionInteractor.onCollectionMenuOpened] and [TopSiteInteractor.onTopSiteMenuOpened]
     */
    fun handleMenuOpened()
Matthew Finkel's avatar
Matthew Finkel committed
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204

    /**
     * @see [TorBootstrapInteractor.onTorBootstrapConnectClicked]
     */
    fun handleTorBootstrapConnectClicked()

    /**
     * @see [TorBootstrapInteractor.onTorStopBootstrapping]
     */
    fun handleTorStopBootstrapping()

    /**
     * @see [TorBootstrapInteractor.onTorStartBootstrapping]
     */
    fun handleTorStartBootstrapping()

    /**
     * @see [TorBootstrapInteractor.onTorStartDebugBootstrapping]
     */
    fun handleTorStartDebugBootstrapping()

    /**
     * @see [TorBootstrapInteractor.onTorBootstrapNetworkSettingsClicked]
     */
    fun handleTorNetworkSettingsClicked()
205
206
}

207
@Suppress("TooManyFunctions", "LargeClass")
208
209
class DefaultSessionControlController(
    private val activity: HomeActivity,
210
    private val settings: Settings,
211
212
    private val engine: Engine,
    private val metrics: MetricController,
213
    private val store: BrowserStore,
214
215
    private val tabCollectionStorage: TabCollectionStorage,
    private val addTabUseCase: TabsUseCases.AddNewTabUseCase,
216
    private val restoreUseCase: TabsUseCases.RestoreUseCase,
217
    private val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
218
    private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
219
    private val fragmentStore: HomeFragmentStore,
220
    private val navController: NavController,
221
    private val viewLifecycleScope: CoroutineScope,
222
223
    private val hideOnboarding: () -> Unit,
    private val registerCollectionStorageObserver: () -> Unit,
224
225
226
227
228
229
230
231
    private val showDeleteCollectionPrompt: (
        tabCollection: TabCollection,
        title: String?,
        message: String,
        wasSwiped: Boolean,
        handleSwipedItemDeletionCancel: () -> Unit
    ) -> Unit,
    private val showTabTray: () -> Unit,
Matthew Finkel's avatar
Matthew Finkel committed
232
233
234
235
236
    private val handleSwipedItemDeletionCancel: () -> Unit,
    private val handleTorBootstrapConnect: () -> Unit,
    private val initiateTorBootstrap: (Boolean) -> Unit,
    private val cancelTorBootstrap: () -> Unit,
    private val openTorNetworkSettings: () -> Unit
237
238
239
240
241
242
243
244
245
246
) : SessionControlController {

    override fun handleCollectionAddTabTapped(collection: TabCollection) {
        metrics.track(Event.CollectionAddTabPressed)
        showCollectionCreationFragment(
            step = SaveCollectionStep.SelectTabs,
            selectedTabCollectionId = collection.id
        )
    }

247
248
249
250
    override fun handleMenuOpened() {
        dismissSearchDialogIfDisplayed()
    }

251
    override fun handleCollectionOpenTabClicked(tab: ComponentTab) {
252
        dismissSearchDialogIfDisplayed()
253
254

        restoreUseCase.invoke(
255
            activity,
256
            engine,
257
258
259
            tab,
            onTabRestored = {
                activity.openToBrowser(BrowserDirection.FromHome)
260
261
                selectTabUseCase.invoke(it)
                reloadUrlUseCase.invoke(it)
262
263
264
265
266
267
268
269
            },
            onFailure = {
                activity.openToBrowserAndLoad(
                    searchTermOrURL = tab.url,
                    newTab = true,
                    from = BrowserDirection.FromHome
                )
            }
270
271
272
273
274
275
        )

        metrics.track(Event.CollectionTabRestored)
    }

    override fun handleCollectionOpenTabsTapped(collection: TabCollection) {
276
        restoreUseCase.invoke(
277
            activity,
278
            engine,
279
280
            collection,
            onFailure = { url ->
281
                addTabUseCase.invoke(url)
282
            }
283
        )
284

285
        showTabTray()
286
287
288
        metrics.track(Event.CollectionAllTabsRestored)
    }

289
290
291
292
293
    override fun handleCollectionRemoveTab(
        collection: TabCollection,
        tab: ComponentTab,
        wasSwiped: Boolean
    ) {
294
295
        metrics.track(Event.CollectionTabRemoved)

296
        if (collection.tabs.size == 1) {
297
298
299
300
301
302
            val title = activity.resources.getString(
                R.string.delete_tab_and_collection_dialog_title,
                collection.title
            )
            val message =
                activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
303
304
305
306
307
308
309
            showDeleteCollectionPrompt(
                collection,
                title,
                message,
                wasSwiped,
                handleSwipedItemDeletionCancel
            )
310
        } else {
311
            viewLifecycleScope.launch {
312
313
                tabCollectionStorage.removeTabFromCollection(collection, tab)
            }
314
315
316
317
        }
    }

    override fun handleCollectionShareTabsClicked(collection: TabCollection) {
318
        dismissSearchDialogIfDisplayed()
319
320
321
322
        showShareFragment(
            collection.title,
            collection.tabs.map { ShareData(url = it.url, title = it.title) }
        )
323
324
325
326
        metrics.track(Event.CollectionShared)
    }

    override fun handleDeleteCollectionTapped(collection: TabCollection) {
327
328
        val message =
            activity.resources.getString(R.string.tab_collection_dialog_message, collection.title)
329
        showDeleteCollectionPrompt(collection, null, message, false, handleSwipedItemDeletionCancel)
330
331
    }

332
    override fun handleOpenInPrivateTabClicked(topSite: TopSite) {
333
        metrics.track(Event.TopSiteOpenInPrivateTab)
334
335
336
337
338
339
340
341
342
343
        with(activity) {
            browsingModeManager.mode = BrowsingMode.Private
            openToBrowserAndLoad(
                searchTermOrURL = topSite.url,
                newTab = true,
                from = BrowserDirection.FromHome
            )
        }
    }

344
    override fun handlePrivateBrowsingLearnMoreClicked() {
345
        dismissSearchDialogIfDisplayed()
346
347
348
349
350
351
352
353
        activity.openToBrowserAndLoad(
            searchTermOrURL = SupportUtils.getGenericSumoURLForTopic
                (SupportUtils.SumoTopic.PRIVATE_BROWSING_MYTHS),
            newTab = true,
            from = BrowserDirection.FromHome
        )
    }

354
355
356
    override fun handleRenameTopSiteClicked(topSite: TopSite) {
        activity.let {
            val customLayout =
357
                LayoutInflater.from(it).inflate(R.layout.top_sites_rename_dialog, null)
358
            val topSiteLabelEditText: EditText =
359
                customLayout.findViewById(R.id.top_site_title)
360
361
362
363
364
365
            topSiteLabelEditText.setText(topSite.title)

            AlertDialog.Builder(it).apply {
                setTitle(R.string.rename_top_site)
                setView(customLayout)
                setPositiveButton(R.string.top_sites_rename_dialog_ok) { dialog, _ ->
366
367
                    viewLifecycleScope.launch(Dispatchers.IO) {
                        with(activity.components.useCases.topSitesUseCase) {
368
369
370
371
372
                            updateTopSites(
                                topSite,
                                topSiteLabelEditText.text.toString(),
                                topSite.url
                            )
373
374
375
376
377
378
379
380
381
382
383
384
385
386
                        }
                    }
                    dialog.dismiss()
                }
                setNegativeButton(R.string.top_sites_rename_dialog_cancel) { dialog, _ ->
                    dialog.cancel()
                }
            }.show().also {
                topSiteLabelEditText.setSelection(0, topSiteLabelEditText.text.length)
                topSiteLabelEditText.showKeyboard()
            }
        }
    }

387
    override fun handleRemoveTopSiteClicked(topSite: TopSite) {
388
        metrics.track(Event.TopSiteRemoved)
ekager's avatar
ekager committed
389
390
391
        if (topSite.url == SupportUtils.POCKET_TRENDING_URL) {
            metrics.track(Event.PocketTopSiteRemoved)
        }
392

393
        viewLifecycleScope.launch(Dispatchers.IO) {
394
395
396
            with(activity.components.useCases.topSitesUseCase) {
                removeTopSites(topSite)
            }
397
398
399
        }
    }

400
401
402
403
404
405
406
407
    override fun handleRenameCollectionTapped(collection: TabCollection) {
        showCollectionCreationFragment(
            step = SaveCollectionStep.RenameCollection,
            selectedTabCollectionId = collection.id
        )
        metrics.track(Event.CollectionRenamePressed)
    }

408
    override fun handleSelectTopSite(url: String, type: TopSite.Type) {
409
        dismissSearchDialogIfDisplayed()
410

411
        metrics.track(Event.TopSiteOpenInNewTab)
412

413
414
415
416
        when (type) {
            TopSite.Type.DEFAULT -> metrics.track(Event.TopSiteOpenDefault)
            TopSite.Type.FRECENT -> metrics.track(Event.TopSiteOpenFrecent)
            TopSite.Type.PINNED -> metrics.track(Event.TopSiteOpenPinned)
417
        }
418

419
420
421
422
        if (url == SupportUtils.GOOGLE_URL) {
            metrics.track(Event.TopSiteOpenGoogle)
        }

423
424
425
        if (url == SupportUtils.POCKET_TRENDING_URL) {
            metrics.track(Event.PocketTopSiteClicked)
        }
426

427
        val availableEngines = getAvailableSearchEngines()
428

429
430
431
432
433
434
435
436
437
438
        val searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.TOPSITE
        val event =
            availableEngines.firstOrNull {
                    engine -> engine.resultUrls.firstOrNull { it.contains(url) } != null
            }?.let {
                    searchEngine -> searchAccessPoint.let { sap ->
                    MetricsUtils.createSearchEvent(searchEngine, store, sap)
                }
            }
        event?.let { activity.metrics.track(it) }
439

440
        addTabUseCase.invoke(
441
            url = appendSearchAttributionToUrlIfNeeded(url),
442
443
            selectTab = true,
            startLoading = true
444
        )
445
        activity.openToBrowser(BrowserDirection.FromHome)
446
447
    }

448
    @VisibleForTesting
449
450
451
    internal fun getAvailableSearchEngines() =
        activity.components.core.store.state.search.searchEngines +
                activity.components.core.store.state.search.availableSearchEngines
452

453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
    /**
     * Append a search attribution query to any provided search engine URL based on the
     * user's current region.
     */
    private fun appendSearchAttributionToUrlIfNeeded(url: String): String {
        if (url == SupportUtils.GOOGLE_URL) {
            store.state.search.region?.let { region ->
                return when (region.current) {
                    "US" -> SupportUtils.GOOGLE_US_URL
                    else -> SupportUtils.GOOGLE_XX_URL
                }
            }
        }

        return url
    }

470
471
472
473
474
475
    private fun dismissSearchDialogIfDisplayed() {
        if (navController.currentDestination?.id == R.id.searchDialogFragment) {
            navController.navigateUp()
        }
    }

476
477
478
479
    override fun handleStartBrowsingClicked() {
        hideOnboarding()
    }

480
    override fun handleOpenSettingsClicked() {
481
482
        val directions = HomeFragmentDirections.actionGlobalPrivateBrowsingFragment()
        navController.nav(R.id.homeFragment, directions)
483
484
    }

Matthew Finkel's avatar
Matthew Finkel committed
485
    override fun handleOpenSecurityLevelSettingsClicked() {
Matthew Finkel's avatar
Matthew Finkel committed
486
487
        val directions = HomeFragmentDirections.actionGlobalTorSecurityLevelFragment()
        navController.nav(R.id.homeFragment, directions)
Matthew Finkel's avatar
Matthew Finkel committed
488
489
    }

490
    override fun handleWhatsNewGetAnswersClicked() {
491
492
493
494
495
        activity.openToBrowserAndLoad(
            searchTermOrURL = SupportUtils.getWhatsNewUrl(activity),
            newTab = true,
            from = BrowserDirection.FromHome
        )
496
497
498
    }

    override fun handleReadPrivacyNoticeClicked() {
499
500
501
502
503
        activity.openToBrowserAndLoad(
            searchTermOrURL = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
            newTab = true,
            from = BrowserDirection.FromHome
        )
504
505
    }

Matthew Finkel's avatar
Matthew Finkel committed
506
507
508
509
510
511
512
513
    override fun handleDonateClicked() {
        activity.openToBrowserAndLoad(
            searchTermOrURL = SupportUtils.DONATE_URL,
            newTab = true,
            from = BrowserDirection.FromHome
        )
    }

514
    override fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean) {
515
        fragmentStore.dispatch(HomeFragmentAction.CollectionExpanded(collection, expand))
516
517
    }

518
519
520
521
    override fun handleCloseTip(tip: Tip) {
        fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip))
    }

522
523
524
525
526
527
528
    private fun showTabTrayCollectionCreation() {
        val directions = HomeFragmentDirections.actionGlobalTabTrayDialogFragment(
            enterMultiselect = true
        )
        navController.nav(R.id.homeFragment, directions)
    }

529
530
531
532
533
534
535
536
537
538
    private fun showCollectionCreationFragment(
        step: SaveCollectionStep,
        selectedTabIds: Array<String>? = null,
        selectedTabCollectionId: Long? = null
    ) {
        if (navController.currentDestination?.id == R.id.collectionCreationFragment) return

        // Only register the observer right before moving to collection creation
        registerCollectionStorageObserver()

539
540
        val tabIds = store.state
            .getNormalOrPrivateTabs(private = activity.browsingModeManager.mode.isPrivate)
541
542
543
            .map { session -> session.id }
            .toList()
            .toTypedArray()
544
        val directions = HomeFragmentDirections.actionGlobalCollectionCreationFragment(
545
546
547
548
549
550
551
552
            tabIds = tabIds,
            saveCollectionStep = step,
            selectedTabIds = selectedTabIds,
            selectedTabCollectionId = selectedTabCollectionId ?: -1
        )
        navController.nav(R.id.homeFragment, directions)
    }

553
    override fun handleCreateCollection() {
554
        showTabTrayCollectionCreation()
555
556
    }

557
558
559
560
561
    override fun handleRemoveCollectionsPlaceholder() {
        settings.showCollectionsPlaceholderOnHome = false
        fragmentStore.dispatch(HomeFragmentAction.RemoveCollectionsPlaceholder)
    }

562
    private fun showShareFragment(shareSubject: String, data: List<ShareData>) {
Jeff Boek's avatar
Jeff Boek committed
563
        val directions = HomeFragmentDirections.actionGlobalShareFragment(
564
            shareSubject = shareSubject,
565
566
567
568
            data = data.toTypedArray()
        )
        navController.nav(R.id.homeFragment, directions)
    }
569
570

    override fun handlePasteAndGo(clipboardText: String) {
571
572
        val searchEngine = store.state.search.selectedOrDefaultSearchEngine

573
574
575
576
        activity.openToBrowserAndLoad(
            searchTermOrURL = clipboardText,
            newTab = true,
            from = BrowserDirection.FromHome,
577
            engine = searchEngine
578
579
        )

580
        val event = if (clipboardText.isUrl() || searchEngine == null) {
581
582
583
584
585
            Event.EnteredUrl(false)
        } else {
            val searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.ACTION
            searchAccessPoint.let { sap ->
                MetricsUtils.createSearchEvent(
586
587
                    searchEngine,
                    store,
588
589
590
591
592
593
594
595
596
                    sap
                )
            }
        }

        event?.let { activity.metrics.track(it) }
    }

    override fun handlePaste(clipboardText: String) {
597
        val directions = HomeFragmentDirections.actionGlobalSearchDialog(
598
599
600
601
602
            sessionId = null,
            pastedText = clipboardText
        )
        navController.nav(R.id.homeFragment, directions)
    }
Matthew Finkel's avatar
Matthew Finkel committed
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622

    override fun handleTorBootstrapConnectClicked() {
        handleTorBootstrapConnect()
    }

    override fun handleTorStopBootstrapping() {
        cancelTorBootstrap()
    }

    override fun handleTorStartBootstrapping() {
        initiateTorBootstrap(false)
    }

    override fun handleTorStartDebugBootstrapping() {
        initiateTorBootstrap(true)
    }

    override fun handleTorNetworkSettingsClicked() {
        openTorNetworkSettings()
    }
623
}