SessionControlController.kt 17.5 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
9
import android.view.LayoutInflater
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
10
11
12
13
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
14
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
15
16
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore
17
import mozilla.components.concept.engine.Engine
18
import mozilla.components.concept.engine.prompt.ShareData
19
import mozilla.components.feature.session.SessionUseCases
20
import mozilla.components.feature.tab.collections.TabCollection
21
import mozilla.components.feature.tab.collections.ext.invoke
22
import mozilla.components.feature.tabs.TabsUseCases
23
import mozilla.components.feature.top.sites.TopSite
24
import mozilla.components.support.ktx.android.view.showKeyboard
25
import mozilla.components.support.ktx.kotlin.isUrl
26
27
28
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
29
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
30
31
32
33
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
34
import org.mozilla.fenix.components.metrics.MetricsUtils
35
import org.mozilla.fenix.components.tips.Tip
36
37
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
38
39
40
41
42
43
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
44
import org.mozilla.fenix.utils.Settings
ekager's avatar
ekager committed
45
import mozilla.components.feature.tab.collections.Tab as ComponentTab
46
47
48
49
50

/**
 * [HomeFragment] controller. An interface that handles the view manipulation of the Tabs triggered
 * by the Interactor.
 */
51
@Suppress("TooManyFunctions")
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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]
     */
71
    fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab, wasSwiped: Boolean)
72
73
74
75
76
77
78
79
80
81
82

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

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

83
84
85
86
87
    /**
     * @see [TopSiteInteractor.onOpenInPrivateTabClicked]
     */
    fun handleOpenInPrivateTabClicked(topSite: TopSite)

88
89
90
91
92
    /**
     * @see [TabSessionInteractor.onPrivateBrowsingLearnMoreClicked]
     */
    fun handlePrivateBrowsingLearnMoreClicked()

93
94
95
96
97
    /**
     * @see [TopSiteInteractor.onRenameTopSiteClicked]
     */
    fun handleRenameTopSiteClicked(topSite: TopSite)

98
99
100
101
102
    /**
     * @see [TopSiteInteractor.onRemoveTopSiteClicked]
     */
    fun handleRemoveTopSiteClicked(topSite: TopSite)

103
104
105
106
107
    /**
     * @see [CollectionInteractor.onRenameCollectionTapped]
     */
    fun handleRenameCollectionTapped(collection: TabCollection)

108
109
110
    /**
     * @see [TopSiteInteractor.onSelectTopSite]
     */
111
    fun handleSelectTopSite(url: String, type: TopSite.Type)
112

113
114
115
116
117
    /**
     * @see [OnboardingInteractor.onStartBrowsingClicked]
     */
    fun handleStartBrowsingClicked()

118
119
120
121
122
    /**
     * @see [OnboardingInteractor.onOpenSettingsClicked]
     */
    fun handleOpenSettingsClicked()

123
124
125
126
127
128
129
130
131
132
    /**
     * @see [OnboardingInteractor.onWhatsNewGetAnswersClicked]
     */
    fun handleWhatsNewGetAnswersClicked()

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

133
134
135
136
    /**
     * @see [CollectionInteractor.onToggleCollectionExpanded]
     */
    fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean)
137

138
139
140
    /**
     * @see [TipInteractor.onCloseTip]
     */
141
    fun handleCloseTip(tip: Tip)
142

143
144
145
146
147
148
149
150
151
152
    /**
     * @see [ToolbarInteractor.onPasteAndGo]
     */
    fun handlePasteAndGo(clipboardText: String)

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

153
154
155
156
    /**
     * @see [CollectionInteractor.onAddTabsToCollectionTapped]
     */
    fun handleCreateCollection()
157
158
159
160
161

    /**
     * @see [CollectionInteractor.onRemoveCollectionsPlaceholder]
     */
    fun handleRemoveCollectionsPlaceholder()
162
163
164
165
166

    /**
     * @see [CollectionInteractor.onCollectionMenuOpened] and [TopSiteInteractor.onTopSiteMenuOpened]
     */
    fun handleMenuOpened()
167
168
}

169
@Suppress("TooManyFunctions", "LargeClass")
170
171
class DefaultSessionControlController(
    private val activity: HomeActivity,
172
    private val settings: Settings,
173
174
    private val engine: Engine,
    private val metrics: MetricController,
175
    private val store: BrowserStore,
176
177
    private val tabCollectionStorage: TabCollectionStorage,
    private val addTabUseCase: TabsUseCases.AddNewTabUseCase,
178
    private val restoreUseCase: TabsUseCases.RestoreUseCase,
179
    private val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
180
    private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
181
    private val fragmentStore: HomeFragmentStore,
182
    private val navController: NavController,
183
    private val viewLifecycleScope: CoroutineScope,
184
185
    private val hideOnboarding: () -> Unit,
    private val registerCollectionStorageObserver: () -> Unit,
186
187
188
189
190
191
192
193
194
    private val showDeleteCollectionPrompt: (
        tabCollection: TabCollection,
        title: String?,
        message: String,
        wasSwiped: Boolean,
        handleSwipedItemDeletionCancel: () -> Unit
    ) -> Unit,
    private val showTabTray: () -> Unit,
    private val handleSwipedItemDeletionCancel: () -> Unit
195
196
197
198
199
200
201
202
203
204
) : SessionControlController {

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

205
206
207
208
    override fun handleMenuOpened() {
        dismissSearchDialogIfDisplayed()
    }

209
    override fun handleCollectionOpenTabClicked(tab: ComponentTab) {
210
        dismissSearchDialogIfDisplayed()
211
212

        restoreUseCase.invoke(
213
            activity,
214
            engine,
215
216
217
            tab,
            onTabRestored = {
                activity.openToBrowser(BrowserDirection.FromHome)
218
219
220
221
                store.state.selectedTabId?.let {
                    selectTabUseCase.invoke(it)
                    reloadUrlUseCase.invoke(it)
                }
222
223
224
225
226
227
228
229
            },
            onFailure = {
                activity.openToBrowserAndLoad(
                    searchTermOrURL = tab.url,
                    newTab = true,
                    from = BrowserDirection.FromHome
                )
            }
230
231
232
233
234
235
        )

        metrics.track(Event.CollectionTabRestored)
    }

    override fun handleCollectionOpenTabsTapped(collection: TabCollection) {
236
        restoreUseCase.invoke(
237
            activity,
238
            engine,
239
240
            collection,
            onFailure = { url ->
241
                addTabUseCase.invoke(url)
242
            }
243
        )
244

245
        showTabTray()
246
247
248
        metrics.track(Event.CollectionAllTabsRestored)
    }

249
250
251
252
253
    override fun handleCollectionRemoveTab(
        collection: TabCollection,
        tab: ComponentTab,
        wasSwiped: Boolean
    ) {
254
255
        metrics.track(Event.CollectionTabRemoved)

256
        if (collection.tabs.size == 1) {
257
258
259
260
261
262
            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)
263
264
265
266
267
268
269
            showDeleteCollectionPrompt(
                collection,
                title,
                message,
                wasSwiped,
                handleSwipedItemDeletionCancel
            )
270
        } else {
271
            viewLifecycleScope.launch {
272
273
                tabCollectionStorage.removeTabFromCollection(collection, tab)
            }
274
275
276
277
        }
    }

    override fun handleCollectionShareTabsClicked(collection: TabCollection) {
278
        dismissSearchDialogIfDisplayed()
279
280
281
282
        showShareFragment(
            collection.title,
            collection.tabs.map { ShareData(url = it.url, title = it.title) }
        )
283
284
285
286
        metrics.track(Event.CollectionShared)
    }

    override fun handleDeleteCollectionTapped(collection: TabCollection) {
287
288
        val message =
            activity.resources.getString(R.string.tab_collection_dialog_message, collection.title)
289
        showDeleteCollectionPrompt(collection, null, message, false, handleSwipedItemDeletionCancel)
290
291
    }

292
    override fun handleOpenInPrivateTabClicked(topSite: TopSite) {
293
        metrics.track(Event.TopSiteOpenInPrivateTab)
294
295
296
297
298
299
300
301
302
303
        with(activity) {
            browsingModeManager.mode = BrowsingMode.Private
            openToBrowserAndLoad(
                searchTermOrURL = topSite.url,
                newTab = true,
                from = BrowserDirection.FromHome
            )
        }
    }

304
    override fun handlePrivateBrowsingLearnMoreClicked() {
305
        dismissSearchDialogIfDisplayed()
306
307
308
309
310
311
312
313
        activity.openToBrowserAndLoad(
            searchTermOrURL = SupportUtils.getGenericSumoURLForTopic
                (SupportUtils.SumoTopic.PRIVATE_BROWSING_MYTHS),
            newTab = true,
            from = BrowserDirection.FromHome
        )
    }

314
315
316
    override fun handleRenameTopSiteClicked(topSite: TopSite) {
        activity.let {
            val customLayout =
317
                LayoutInflater.from(it).inflate(R.layout.top_sites_rename_dialog, null)
318
            val topSiteLabelEditText: EditText =
319
                customLayout.findViewById(R.id.top_site_title)
320
321
322
323
324
325
            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, _ ->
326
327
328
                    viewLifecycleScope.launch(Dispatchers.IO) {
                        with(activity.components.useCases.topSitesUseCase) {
                            renameTopSites(topSite, topSiteLabelEditText.text.toString())
329
330
331
332
333
334
335
336
337
338
339
340
341
342
                        }
                    }
                    dialog.dismiss()
                }
                setNegativeButton(R.string.top_sites_rename_dialog_cancel) { dialog, _ ->
                    dialog.cancel()
                }
            }.show().also {
                topSiteLabelEditText.setSelection(0, topSiteLabelEditText.text.length)
                topSiteLabelEditText.showKeyboard()
            }
        }
    }

343
    override fun handleRemoveTopSiteClicked(topSite: TopSite) {
344
        metrics.track(Event.TopSiteRemoved)
ekager's avatar
ekager committed
345
346
347
        if (topSite.url == SupportUtils.POCKET_TRENDING_URL) {
            metrics.track(Event.PocketTopSiteRemoved)
        }
348

349
        viewLifecycleScope.launch(Dispatchers.IO) {
350
351
352
            with(activity.components.useCases.topSitesUseCase) {
                removeTopSites(topSite)
            }
353
354
355
        }
    }

356
357
358
359
360
361
362
363
    override fun handleRenameCollectionTapped(collection: TabCollection) {
        showCollectionCreationFragment(
            step = SaveCollectionStep.RenameCollection,
            selectedTabCollectionId = collection.id
        )
        metrics.track(Event.CollectionRenamePressed)
    }

364
    override fun handleSelectTopSite(url: String, type: TopSite.Type) {
365
        dismissSearchDialogIfDisplayed()
366

367
        metrics.track(Event.TopSiteOpenInNewTab)
368

369
370
371
372
        when (type) {
            TopSite.Type.DEFAULT -> metrics.track(Event.TopSiteOpenDefault)
            TopSite.Type.FRECENT -> metrics.track(Event.TopSiteOpenFrecent)
            TopSite.Type.PINNED -> metrics.track(Event.TopSiteOpenPinned)
373
        }
374

375
376
377
378
        if (url == SupportUtils.GOOGLE_URL) {
            metrics.track(Event.TopSiteOpenGoogle)
        }

379
380
381
        if (url == SupportUtils.POCKET_TRENDING_URL) {
            metrics.track(Event.PocketTopSiteClicked)
        }
382

383
        addTabUseCase.invoke(
384
            url = appendSearchAttributionToUrlIfNeeded(url),
385
386
            selectTab = true,
            startLoading = true
387
        )
388
        activity.openToBrowser(BrowserDirection.FromHome)
389
390
    }

391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
    /**
     * 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
    }

408
409
410
411
412
413
    private fun dismissSearchDialogIfDisplayed() {
        if (navController.currentDestination?.id == R.id.searchDialogFragment) {
            navController.navigateUp()
        }
    }

414
415
416
417
    override fun handleStartBrowsingClicked() {
        hideOnboarding()
    }

418
    override fun handleOpenSettingsClicked() {
419
420
        val directions = HomeFragmentDirections.actionGlobalPrivateBrowsingFragment()
        navController.nav(R.id.homeFragment, directions)
421
422
    }

423
    override fun handleWhatsNewGetAnswersClicked() {
424
425
426
427
428
        activity.openToBrowserAndLoad(
            searchTermOrURL = SupportUtils.getWhatsNewUrl(activity),
            newTab = true,
            from = BrowserDirection.FromHome
        )
429
430
431
    }

    override fun handleReadPrivacyNoticeClicked() {
432
433
434
435
436
        activity.openToBrowserAndLoad(
            searchTermOrURL = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
            newTab = true,
            from = BrowserDirection.FromHome
        )
437
438
    }

439
    override fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean) {
440
        fragmentStore.dispatch(HomeFragmentAction.CollectionExpanded(collection, expand))
441
442
    }

443
444
445
446
    override fun handleCloseTip(tip: Tip) {
        fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip))
    }

447
448
449
450
451
452
453
    private fun showTabTrayCollectionCreation() {
        val directions = HomeFragmentDirections.actionGlobalTabTrayDialogFragment(
            enterMultiselect = true
        )
        navController.nav(R.id.homeFragment, directions)
    }

454
455
456
457
458
459
460
461
462
463
    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()

464
465
        val tabIds = store.state
            .getNormalOrPrivateTabs(private = activity.browsingModeManager.mode.isPrivate)
466
467
468
            .map { session -> session.id }
            .toList()
            .toTypedArray()
469
        val directions = HomeFragmentDirections.actionGlobalCollectionCreationFragment(
470
471
472
473
474
475
476
477
            tabIds = tabIds,
            saveCollectionStep = step,
            selectedTabIds = selectedTabIds,
            selectedTabCollectionId = selectedTabCollectionId ?: -1
        )
        navController.nav(R.id.homeFragment, directions)
    }

478
    override fun handleCreateCollection() {
479
        showTabTrayCollectionCreation()
480
481
    }

482
483
484
485
486
    override fun handleRemoveCollectionsPlaceholder() {
        settings.showCollectionsPlaceholderOnHome = false
        fragmentStore.dispatch(HomeFragmentAction.RemoveCollectionsPlaceholder)
    }

487
    private fun showShareFragment(shareSubject: String, data: List<ShareData>) {
Jeff Boek's avatar
Jeff Boek committed
488
        val directions = HomeFragmentDirections.actionGlobalShareFragment(
489
            shareSubject = shareSubject,
490
491
492
493
            data = data.toTypedArray()
        )
        navController.nav(R.id.homeFragment, directions)
    }
494
495

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

498
499
500
501
        activity.openToBrowserAndLoad(
            searchTermOrURL = clipboardText,
            newTab = true,
            from = BrowserDirection.FromHome,
502
            engine = searchEngine
503
504
        )

505
        val event = if (clipboardText.isUrl() || searchEngine == null) {
506
507
508
509
510
            Event.EnteredUrl(false)
        } else {
            val searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.ACTION
            searchAccessPoint.let { sap ->
                MetricsUtils.createSearchEvent(
511
512
                    searchEngine,
                    store,
513
514
515
516
517
518
519
520
521
                    sap
                )
            }
        }

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

    override fun handlePaste(clipboardText: String) {
522
        val directions = HomeFragmentDirections.actionGlobalSearchDialog(
523
524
525
526
527
            sessionId = null,
            pastedText = clipboardText
        )
        navController.nav(R.id.homeFragment, directions)
    }
528
}