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 selectTabUseCase: TabsUseCases.SelectTabUseCase,
183
    private val fragmentStore: HomeFragmentStore,
184
    private val navController: NavController,
185
    private val viewLifecycleScope: CoroutineScope,
186
187
    private val hideOnboarding: () -> Unit,
    private val registerCollectionStorageObserver: () -> Unit,
188
189
190
191
192
193
194
195
196
    private val showDeleteCollectionPrompt: (
        tabCollection: TabCollection,
        title: String?,
        message: String,
        wasSwiped: Boolean,
        handleSwipedItemDeletionCancel: () -> Unit
    ) -> Unit,
    private val showTabTray: () -> Unit,
    private val handleSwipedItemDeletionCancel: () -> Unit
197
198
199
200
201
202
203
204
205
206
) : SessionControlController {

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

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

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

        restoreUseCase.invoke(
215
            activity,
216
            engine,
217
218
219
            tab,
            onTabRestored = {
                activity.openToBrowser(BrowserDirection.FromHome)
220
                sessionManager.selectedSession?.let { selectTabUseCase.invoke(it) }
221
                reloadUrlUseCase.invoke(sessionManager.selectedSession)
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
        metrics.track(Event.TopSiteOpenInNewTab)
367
368
369
370
        when (type) {
            TopSite.Type.DEFAULT -> metrics.track(Event.TopSiteOpenDefault)
            TopSite.Type.FRECENT -> metrics.track(Event.TopSiteOpenFrecent)
            TopSite.Type.PINNED -> metrics.track(Event.TopSiteOpenPinned)
371
        }
372

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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