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
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

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
        if (url == SupportUtils.POCKET_TRENDING_URL) {
            metrics.track(Event.PocketTopSiteClicked)
        }
378

379
        addTabUseCase.invoke(
380
            url = appendSearchAttributionToUrlIfNeeded(url),
381
382
            selectTab = true,
            startLoading = true
383
        )
384
        activity.openToBrowser(BrowserDirection.FromHome)
385
386
    }

387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
    /**
     * 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
    }

404
405
406
407
408
409
    private fun dismissSearchDialogIfDisplayed() {
        if (navController.currentDestination?.id == R.id.searchDialogFragment) {
            navController.navigateUp()
        }
    }

410
411
412
413
    override fun handleStartBrowsingClicked() {
        hideOnboarding()
    }

414
    override fun handleOpenSettingsClicked() {
415
416
        val directions = HomeFragmentDirections.actionGlobalPrivateBrowsingFragment()
        navController.nav(R.id.homeFragment, directions)
417
418
    }

419
    override fun handleWhatsNewGetAnswersClicked() {
420
421
422
423
424
        activity.openToBrowserAndLoad(
            searchTermOrURL = SupportUtils.getWhatsNewUrl(activity),
            newTab = true,
            from = BrowserDirection.FromHome
        )
425
426
427
    }

    override fun handleReadPrivacyNoticeClicked() {
428
429
430
431
432
        activity.openToBrowserAndLoad(
            searchTermOrURL = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
            newTab = true,
            from = BrowserDirection.FromHome
        )
433
434
    }

435
    override fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean) {
436
        fragmentStore.dispatch(HomeFragmentAction.CollectionExpanded(collection, expand))
437
438
    }

439
440
441
442
    override fun handleCloseTip(tip: Tip) {
        fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip))
    }

443
444
445
446
447
448
449
    private fun showTabTrayCollectionCreation() {
        val directions = HomeFragmentDirections.actionGlobalTabTrayDialogFragment(
            enterMultiselect = true
        )
        navController.nav(R.id.homeFragment, directions)
    }

450
451
452
453
454
455
456
457
458
459
    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()

460
461
462
463
464
        val tabIds = sessionManager
            .sessionsOfType(private = activity.browsingModeManager.mode.isPrivate)
            .map { session -> session.id }
            .toList()
            .toTypedArray()
465
        val directions = HomeFragmentDirections.actionGlobalCollectionCreationFragment(
466
467
468
469
470
471
472
473
            tabIds = tabIds,
            saveCollectionStep = step,
            selectedTabIds = selectedTabIds,
            selectedTabCollectionId = selectedTabCollectionId ?: -1
        )
        navController.nav(R.id.homeFragment, directions)
    }

474
    override fun handleCreateCollection() {
475
        showTabTrayCollectionCreation()
476
477
    }

478
479
480
481
482
    override fun handleRemoveCollectionsPlaceholder() {
        settings.showCollectionsPlaceholderOnHome = false
        fragmentStore.dispatch(HomeFragmentAction.RemoveCollectionsPlaceholder)
    }

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

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

494
495
496
497
        activity.openToBrowserAndLoad(
            searchTermOrURL = clipboardText,
            newTab = true,
            from = BrowserDirection.FromHome,
498
            engine = searchEngine
499
500
        )

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

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

    override fun handlePaste(clipboardText: String) {
518
        val directions = HomeFragmentDirections.actionGlobalSearchDialog(
519
520
521
522
523
            sessionId = null,
            pastedText = clipboardText
        )
        navController.nav(R.id.homeFragment, directions)
    }
524
}