SessionControlController.kt 16.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
import mozilla.components.concept.engine.Engine
16
17
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.tab.collections.TabCollection
18
import mozilla.components.feature.tab.collections.ext.restore
19
import mozilla.components.feature.tabs.TabsUseCases
20
import mozilla.components.feature.top.sites.TopSite
21
import mozilla.components.support.ktx.android.view.showKeyboard
22
import mozilla.components.support.ktx.kotlin.isUrl
23
24
25
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
26
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
27
28
29
30
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
31
import org.mozilla.fenix.components.metrics.MetricsUtils
32
import org.mozilla.fenix.components.tips.Tip
33
34
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
35
import org.mozilla.fenix.ext.nav
36
import org.mozilla.fenix.ext.sessionsOfType
37
38
39
40
41
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
42
import org.mozilla.fenix.utils.Settings
ekager's avatar
ekager committed
43
import mozilla.components.feature.tab.collections.Tab as ComponentTab
44
45
46
47
48

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

200
201
202
203
    override fun handleMenuOpened() {
        dismissSearchDialogIfDisplayed()
    }

204
    override fun handleCollectionOpenTabClicked(tab: ComponentTab) {
205
        dismissSearchDialogIfDisplayed()
206
207
        sessionManager.restore(
            activity,
208
            engine,
209
210
211
212
213
214
215
216
217
218
219
            tab,
            onTabRestored = {
                activity.openToBrowser(BrowserDirection.FromHome)
            },
            onFailure = {
                activity.openToBrowserAndLoad(
                    searchTermOrURL = tab.url,
                    newTab = true,
                    from = BrowserDirection.FromHome
                )
            }
220
221
222
223
224
225
        )

        metrics.track(Event.CollectionTabRestored)
    }

    override fun handleCollectionOpenTabsTapped(collection: TabCollection) {
226
227
        sessionManager.restore(
            activity,
228
            engine,
229
230
            collection,
            onFailure = { url ->
231
                addTabUseCase.invoke(url)
232
            }
233
        )
234

235
        showTabTray()
236
237
238
        metrics.track(Event.CollectionAllTabsRestored)
    }

239
240
241
242
243
    override fun handleCollectionRemoveTab(
        collection: TabCollection,
        tab: ComponentTab,
        wasSwiped: Boolean
    ) {
244
245
        metrics.track(Event.CollectionTabRemoved)

246
        if (collection.tabs.size == 1) {
247
248
249
250
251
252
            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)
253
254
255
256
257
258
259
            showDeleteCollectionPrompt(
                collection,
                title,
                message,
                wasSwiped,
                handleSwipedItemDeletionCancel
            )
260
        } else {
261
            viewLifecycleScope.launch {
262
263
                tabCollectionStorage.removeTabFromCollection(collection, tab)
            }
264
265
266
267
        }
    }

    override fun handleCollectionShareTabsClicked(collection: TabCollection) {
268
        dismissSearchDialogIfDisplayed()
269
270
271
272
        showShareFragment(
            collection.title,
            collection.tabs.map { ShareData(url = it.url, title = it.title) }
        )
273
274
275
276
        metrics.track(Event.CollectionShared)
    }

    override fun handleDeleteCollectionTapped(collection: TabCollection) {
277
278
        val message =
            activity.resources.getString(R.string.tab_collection_dialog_message, collection.title)
279
        showDeleteCollectionPrompt(collection, null, message, false, handleSwipedItemDeletionCancel)
280
281
    }

282
    override fun handleOpenInPrivateTabClicked(topSite: TopSite) {
283
        metrics.track(Event.TopSiteOpenInPrivateTab)
284
285
286
287
288
289
290
291
292
293
        with(activity) {
            browsingModeManager.mode = BrowsingMode.Private
            openToBrowserAndLoad(
                searchTermOrURL = topSite.url,
                newTab = true,
                from = BrowserDirection.FromHome
            )
        }
    }

294
    override fun handlePrivateBrowsingLearnMoreClicked() {
295
        dismissSearchDialogIfDisplayed()
296
297
298
299
300
301
302
303
        activity.openToBrowserAndLoad(
            searchTermOrURL = SupportUtils.getGenericSumoURLForTopic
                (SupportUtils.SumoTopic.PRIVATE_BROWSING_MYTHS),
            newTab = true,
            from = BrowserDirection.FromHome
        )
    }

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

336
    override fun handleRemoveTopSiteClicked(topSite: TopSite) {
337
        metrics.track(Event.TopSiteRemoved)
ekager's avatar
ekager committed
338
339
340
        if (topSite.url == SupportUtils.POCKET_TRENDING_URL) {
            metrics.track(Event.PocketTopSiteRemoved)
        }
341

342
        viewLifecycleScope.launch(Dispatchers.IO) {
343
344
345
            with(activity.components.useCases.topSitesUseCase) {
                removeTopSites(topSite)
            }
346
347
348
        }
    }

349
350
351
352
353
354
355
356
    override fun handleRenameCollectionTapped(collection: TabCollection) {
        showCollectionCreationFragment(
            step = SaveCollectionStep.RenameCollection,
            selectedTabCollectionId = collection.id
        )
        metrics.track(Event.CollectionRenamePressed)
    }

357
    override fun handleSelectTopSite(url: String, type: TopSite.Type) {
358
        dismissSearchDialogIfDisplayed()
359
        metrics.track(Event.TopSiteOpenInNewTab)
360
361
362
363
        when (type) {
            TopSite.Type.DEFAULT -> metrics.track(Event.TopSiteOpenDefault)
            TopSite.Type.FRECENT -> metrics.track(Event.TopSiteOpenFrecent)
            TopSite.Type.PINNED -> metrics.track(Event.TopSiteOpenPinned)
364
        }
365

366
367
368
        if (url == SupportUtils.POCKET_TRENDING_URL) {
            metrics.track(Event.PocketTopSiteClicked)
        }
369
        addTabUseCase.invoke(
370
371
372
            url = url,
            selectTab = true,
            startLoading = true
373
        )
374
        activity.openToBrowser(BrowserDirection.FromHome)
375
376
    }

377
378
379
380
381
382
    private fun dismissSearchDialogIfDisplayed() {
        if (navController.currentDestination?.id == R.id.searchDialogFragment) {
            navController.navigateUp()
        }
    }

383
384
385
386
    override fun handleStartBrowsingClicked() {
        hideOnboarding()
    }

387
    override fun handleOpenSettingsClicked() {
388
389
        val directions = HomeFragmentDirections.actionGlobalPrivateBrowsingFragment()
        navController.nav(R.id.homeFragment, directions)
390
391
    }

392
    override fun handleWhatsNewGetAnswersClicked() {
393
394
395
396
397
        activity.openToBrowserAndLoad(
            searchTermOrURL = SupportUtils.getWhatsNewUrl(activity),
            newTab = true,
            from = BrowserDirection.FromHome
        )
398
399
400
    }

    override fun handleReadPrivacyNoticeClicked() {
401
402
403
404
405
        activity.openToBrowserAndLoad(
            searchTermOrURL = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
            newTab = true,
            from = BrowserDirection.FromHome
        )
406
407
    }

408
    override fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean) {
409
        fragmentStore.dispatch(HomeFragmentAction.CollectionExpanded(collection, expand))
410
411
    }

412
413
414
415
    override fun handleCloseTip(tip: Tip) {
        fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip))
    }

416
417
418
419
420
421
422
    private fun showTabTrayCollectionCreation() {
        val directions = HomeFragmentDirections.actionGlobalTabTrayDialogFragment(
            enterMultiselect = true
        )
        navController.nav(R.id.homeFragment, directions)
    }

423
424
425
426
427
428
429
430
431
432
    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()

433
434
435
436
437
        val tabIds = sessionManager
            .sessionsOfType(private = activity.browsingModeManager.mode.isPrivate)
            .map { session -> session.id }
            .toList()
            .toTypedArray()
438
        val directions = HomeFragmentDirections.actionGlobalCollectionCreationFragment(
439
440
441
442
443
444
445
446
            tabIds = tabIds,
            saveCollectionStep = step,
            selectedTabIds = selectedTabIds,
            selectedTabCollectionId = selectedTabCollectionId ?: -1
        )
        navController.nav(R.id.homeFragment, directions)
    }

447
    override fun handleCreateCollection() {
448
        showTabTrayCollectionCreation()
449
450
    }

451
452
453
454
455
    override fun handleRemoveCollectionsPlaceholder() {
        settings.showCollectionsPlaceholderOnHome = false
        fragmentStore.dispatch(HomeFragmentAction.RemoveCollectionsPlaceholder)
    }

456
    private fun showShareFragment(shareSubject: String, data: List<ShareData>) {
Jeff Boek's avatar
Jeff Boek committed
457
        val directions = HomeFragmentDirections.actionGlobalShareFragment(
458
            shareSubject = shareSubject,
459
460
461
462
            data = data.toTypedArray()
        )
        navController.nav(R.id.homeFragment, directions)
    }
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488

    override fun handlePasteAndGo(clipboardText: String) {
        activity.openToBrowserAndLoad(
            searchTermOrURL = clipboardText,
            newTab = true,
            from = BrowserDirection.FromHome,
            engine = activity.components.search.provider.getDefaultEngine(activity)
        )

        val event = if (clipboardText.isUrl()) {
            Event.EnteredUrl(false)
        } else {
            val searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.ACTION
            searchAccessPoint.let { sap ->
                MetricsUtils.createSearchEvent(
                    activity.components.search.provider.getDefaultEngine(activity),
                    activity,
                    sap
                )
            }
        }

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

    override fun handlePaste(clipboardText: String) {
489
        val directions = HomeFragmentDirections.actionGlobalSearchDialog(
490
491
492
493
494
            sessionId = null,
            pastedText = clipboardText
        )
        navController.nav(R.id.homeFragment, directions)
    }
495
}