SessionControlController.kt 16.9 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
14
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.browser.session.SessionManager
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
import org.mozilla.fenix.ext.nav
39
import org.mozilla.fenix.ext.sessionsOfType
40
41
42
43
44
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
45
import org.mozilla.fenix.utils.Settings
ekager's avatar
ekager committed
46
import mozilla.components.feature.tab.collections.Tab as ComponentTab
47
48
49
50
51

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        restoreUseCase.invoke(
214
            activity,
215
            engine,
216
217
218
            tab,
            onTabRestored = {
                activity.openToBrowser(BrowserDirection.FromHome)
219
                reloadUrlUseCase.invoke(sessionManager.selectedSession)
220
221
222
223
224
225
226
227
            },
            onFailure = {
                activity.openToBrowserAndLoad(
                    searchTermOrURL = tab.url,
                    newTab = true,
                    from = BrowserDirection.FromHome
                )
            }
228
229
230
231
232
233
        )

        metrics.track(Event.CollectionTabRestored)
    }

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

243
        showTabTray()
244
245
246
        metrics.track(Event.CollectionAllTabsRestored)
    }

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

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

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

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

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

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

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

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

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

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

365
    override fun handleSelectTopSite(url: String, type: TopSite.Type) {
366
        dismissSearchDialogIfDisplayed()
367
        metrics.track(Event.TopSiteOpenInNewTab)
368
369
370
371
        when (type) {
            TopSite.Type.DEFAULT -> metrics.track(Event.TopSiteOpenDefault)
            TopSite.Type.FRECENT -> metrics.track(Event.TopSiteOpenFrecent)
            TopSite.Type.PINNED -> metrics.track(Event.TopSiteOpenPinned)
372
        }
373

374
375
376
        if (url == SupportUtils.POCKET_TRENDING_URL) {
            metrics.track(Event.PocketTopSiteClicked)
        }
377
        addTabUseCase.invoke(
378
379
380
            url = url,
            selectTab = true,
            startLoading = true
381
        )
382
        activity.openToBrowser(BrowserDirection.FromHome)
383
384
    }

385
386
387
388
389
390
    private fun dismissSearchDialogIfDisplayed() {
        if (navController.currentDestination?.id == R.id.searchDialogFragment) {
            navController.navigateUp()
        }
    }

391
392
393
394
    override fun handleStartBrowsingClicked() {
        hideOnboarding()
    }

395
    override fun handleOpenSettingsClicked() {
396
397
        val directions = HomeFragmentDirections.actionGlobalPrivateBrowsingFragment()
        navController.nav(R.id.homeFragment, directions)
398
399
    }

400
    override fun handleWhatsNewGetAnswersClicked() {
401
402
403
404
405
        activity.openToBrowserAndLoad(
            searchTermOrURL = SupportUtils.getWhatsNewUrl(activity),
            newTab = true,
            from = BrowserDirection.FromHome
        )
406
407
408
    }

    override fun handleReadPrivacyNoticeClicked() {
409
410
411
412
413
        activity.openToBrowserAndLoad(
            searchTermOrURL = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
            newTab = true,
            from = BrowserDirection.FromHome
        )
414
415
    }

416
    override fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean) {
417
        fragmentStore.dispatch(HomeFragmentAction.CollectionExpanded(collection, expand))
418
419
    }

420
421
422
423
    override fun handleCloseTip(tip: Tip) {
        fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip))
    }

424
425
426
427
428
429
430
    private fun showTabTrayCollectionCreation() {
        val directions = HomeFragmentDirections.actionGlobalTabTrayDialogFragment(
            enterMultiselect = true
        )
        navController.nav(R.id.homeFragment, directions)
    }

431
432
433
434
435
436
437
438
439
440
    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()

441
442
443
444
445
        val tabIds = sessionManager
            .sessionsOfType(private = activity.browsingModeManager.mode.isPrivate)
            .map { session -> session.id }
            .toList()
            .toTypedArray()
446
        val directions = HomeFragmentDirections.actionGlobalCollectionCreationFragment(
447
448
449
450
451
452
453
454
            tabIds = tabIds,
            saveCollectionStep = step,
            selectedTabIds = selectedTabIds,
            selectedTabCollectionId = selectedTabCollectionId ?: -1
        )
        navController.nav(R.id.homeFragment, directions)
    }

455
    override fun handleCreateCollection() {
456
        showTabTrayCollectionCreation()
457
458
    }

459
460
461
462
463
    override fun handleRemoveCollectionsPlaceholder() {
        settings.showCollectionsPlaceholderOnHome = false
        fragmentStore.dispatch(HomeFragmentAction.RemoveCollectionsPlaceholder)
    }

464
    private fun showShareFragment(shareSubject: String, data: List<ShareData>) {
Jeff Boek's avatar
Jeff Boek committed
465
        val directions = HomeFragmentDirections.actionGlobalShareFragment(
466
            shareSubject = shareSubject,
467
468
469
470
            data = data.toTypedArray()
        )
        navController.nav(R.id.homeFragment, directions)
    }
471
472

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

475
476
477
478
        activity.openToBrowserAndLoad(
            searchTermOrURL = clipboardText,
            newTab = true,
            from = BrowserDirection.FromHome,
479
            engine = searchEngine
480
481
        )

482
        val event = if (clipboardText.isUrl() || searchEngine == null) {
483
484
485
486
487
            Event.EnteredUrl(false)
        } else {
            val searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.ACTION
            searchAccessPoint.let { sap ->
                MetricsUtils.createSearchEvent(
488
489
                    searchEngine,
                    store,
490
491
492
493
494
495
496
497
498
                    sap
                )
            }
        }

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

    override fun handlePaste(clipboardText: String) {
499
        val directions = HomeFragmentDirections.actionGlobalSearchDialog(
500
501
502
503
504
            sessionId = null,
            pastedText = clipboardText
        )
        navController.nav(R.id.homeFragment, directions)
    }
505
}