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.restore
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 reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
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
        sessionManager.restore(
            activity,
213
            engine,
214
215
216
            tab,
            onTabRestored = {
                activity.openToBrowser(BrowserDirection.FromHome)
217
                reloadUrlUseCase.invoke(sessionManager.selectedSession)
218
219
220
221
222
223
224
225
            },
            onFailure = {
                activity.openToBrowserAndLoad(
                    searchTermOrURL = tab.url,
                    newTab = true,
                    from = BrowserDirection.FromHome
                )
            }
226
227
228
229
230
231
        )

        metrics.track(Event.CollectionTabRestored)
    }

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

241
        showTabTray()
242
243
244
        metrics.track(Event.CollectionAllTabsRestored)
    }

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

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

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

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

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

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

310
311
312
    override fun handleRenameTopSiteClicked(topSite: TopSite) {
        activity.let {
            val customLayout =
313
                LayoutInflater.from(it).inflate(R.layout.top_sites_rename_dialog, null)
314
            val topSiteLabelEditText: EditText =
315
                customLayout.findViewById(R.id.top_site_title)
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
            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()
            }
        }
    }

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

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

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

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

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

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

389
390
391
392
    override fun handleStartBrowsingClicked() {
        hideOnboarding()
    }

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

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

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

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

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

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

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

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

453
    override fun handleCreateCollection() {
454
        showTabTrayCollectionCreation()
455
456
    }

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

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

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

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

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

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

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