HomeActivity.kt 34.6 KB
Newer Older
Jeff Boek's avatar
Jeff Boek committed
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
3
 * 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/. */
Jeff Boek's avatar
Jeff Boek committed
4

Jeff Boek's avatar
Jeff Boek committed
5
6
package org.mozilla.fenix

7
import android.content.Context
8
import android.content.Intent
9
import android.content.res.Configuration
10
import android.os.Build
Jeff Boek's avatar
Jeff Boek committed
11
import android.os.Bundle
12
import android.os.StrictMode
13
import android.os.SystemClock
14
import android.text.format.DateUtils
15
import android.util.AttributeSet
16
import android.view.KeyEvent
17
import android.view.View
18
import android.view.ViewConfiguration
19
import android.view.WindowManager
20
import androidx.annotation.CallSuper
21
import androidx.annotation.IdRes
22
23
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PROTECTED
24
import androidx.appcompat.app.ActionBar
25
import androidx.appcompat.widget.Toolbar
26
import androidx.lifecycle.lifecycleScope
27
import androidx.navigation.NavDestination
28
import androidx.navigation.NavDirections
29
import androidx.navigation.fragment.NavHostFragment
30
31
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
32
import kotlinx.android.synthetic.main.activity_home.*
Gabriel Luong's avatar
Gabriel Luong committed
33
import kotlinx.coroutines.CoroutineScope
34
import kotlinx.coroutines.Dispatchers
35
import kotlinx.coroutines.Dispatchers.IO
36
import kotlinx.coroutines.ExperimentalCoroutinesApi
37
38
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
39
import kotlinx.coroutines.launch
40
import mozilla.components.browser.search.SearchEngine
41
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
42
import mozilla.components.browser.state.state.SessionState
Gabriel Luong's avatar
Gabriel Luong committed
43
import mozilla.components.browser.state.state.WebExtensionState
Sebastian Kaspari's avatar
Sebastian Kaspari committed
44
import mozilla.components.concept.engine.EngineSession
45
import mozilla.components.concept.engine.EngineView
46
import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate
47
import mozilla.components.feature.privatemode.notification.PrivateNotificationFeature
48
import mozilla.components.feature.search.BrowserStoreSearchAdapter
Grisha Kruglov's avatar
Grisha Kruglov committed
49
import mozilla.components.service.fxa.sync.SyncReason
50
import mozilla.components.support.base.feature.UserInteractionHandler
51
import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
Sawyer Blatz's avatar
Sawyer Blatz committed
52
53
import mozilla.components.support.ktx.android.content.call
import mozilla.components.support.ktx.android.content.email
54
import mozilla.components.support.ktx.android.content.share
55
56
import mozilla.components.support.ktx.kotlin.isUrl
import mozilla.components.support.ktx.kotlin.toNormalizedUrl
57
import mozilla.components.support.locale.LocaleAwareAppCompatActivity
58
import mozilla.components.support.utils.SafeIntent
59
import mozilla.components.support.utils.toSafeIntent
Gabriel Luong's avatar
Gabriel Luong committed
60
import mozilla.components.support.webextensions.WebExtensionPopupFeature
61
import org.mozilla.fenix.GleanMetrics.Metrics
62
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections
63
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections
64
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
65
66
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
67
import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
68
import org.mozilla.fenix.components.metrics.Event
69
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
70
import org.mozilla.fenix.ext.alreadyOnDestination
71
import org.mozilla.fenix.ext.breadcrumb
72
import org.mozilla.fenix.ext.components
73
import org.mozilla.fenix.ext.metrics
74
import org.mozilla.fenix.ext.nav
75
import org.mozilla.fenix.ext.settings
76
import org.mozilla.fenix.home.HomeFragmentDirections
77
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
78
79
import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor
import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor
80
import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor
81
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
82
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
83
84
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.history.HistoryFragmentDirections
ekager's avatar
ekager committed
85
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
86
import org.mozilla.fenix.perf.Performance
87
import org.mozilla.fenix.perf.StartupTimeline
88
import org.mozilla.fenix.search.SearchDialogFragmentDirections
89
import org.mozilla.fenix.session.PrivateNotificationService
90
import org.mozilla.fenix.settings.SettingsFragmentDirections
91
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
92
import org.mozilla.fenix.settings.about.AboutFragmentDirections
93
import org.mozilla.fenix.settings.logins.fragment.LoginDetailFragmentDirections
94
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections
95
96
97
import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
98
import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
99
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
100
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
101
102
import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager
103
import org.mozilla.fenix.utils.BrowsersCache
104
import java.lang.ref.WeakReference
Jeff Boek's avatar
Jeff Boek committed
105

106
107
108
109
110
111
/**
 * The main activity of the application. The application is primarily a single Activity (this one)
 * with fragments switching out to display different views. The most important views shown here are the:
 * - home screen
 * - browser screen
 */
112
@OptIn(ExperimentalCoroutinesApi::class)
113
@SuppressWarnings("TooManyFunctions", "LargeClass")
114
open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
115
116
117
118
119
    // DO NOT MOVE ANYTHING ABOVE THIS, GETTING INIT TIME IS CRITICAL
    // we need to store startup timestamp for warm startup. we cant directly store
    // inside AppStartupTelemetry since that class lives inside components and
    // components requires context to access.
    protected val homeActivityInitTimeStampNanoSeconds = SystemClock.elapsedRealtimeNanos()
120

Gabriel Luong's avatar
Gabriel Luong committed
121
    private var webExtScope: CoroutineScope? = null
Jeff Boek's avatar
Jeff Boek committed
122
    lateinit var themeManager: ThemeManager
123
    lateinit var browsingModeManager: BrowsingModeManager
124

125
126
    private var isVisuallyComplete = false

127
128
    private var privateNotificationObserver: PrivateNotificationFeature<PrivateNotificationService>? =
        null
129

130
131
    private var isToolbarInflated = false

Gabriel Luong's avatar
Gabriel Luong committed
132
133
134
135
    private val webExtensionPopupFeature by lazy {
        WebExtensionPopupFeature(components.core.store, ::openPopup)
    }

136
137
138
139
    private val navHost by lazy {
        supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment
    }

140
141
    private val externalSourceIntentProcessors by lazy {
        listOf(
142
            SpeechProcessingIntentProcessor(this, components.analytics.metrics),
143
            StartSearchIntentProcessor(components.analytics.metrics),
Sebastian Kaspari's avatar
Sebastian Kaspari committed
144
            DeepLinkIntentProcessor(this, components.analytics.leanplumMetricsService),
145
146
            OpenBrowserIntentProcessor(this, ::getIntentSessionId),
            OpenSpecificTabIntentProcessor(this)
147
148
149
        )
    }

150
151
152
    // See onKeyDown for why this is necessary
    private var backLongPressJob: Job? = null

153
154
    private lateinit var navigationToolbar: Toolbar

155
    final override fun onCreate(savedInstanceState: Bundle?) {
156
        components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager)
157
        // There is disk read violations on some devices such as samsung and pixel for android 9/10
158
        components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
159
160
            super.onCreate(savedInstanceState)
        }
161

162
163
164
165
166
167
168
169
170
171
        // Diagnostic breadcrumb for "Display already aquired" crash:
        // https://github.com/mozilla-mobile/android-components/issues/7960
        breadcrumb(
            message = "onCreate()",
            data = mapOf(
                "recreated" to (savedInstanceState != null).toString(),
                "intent" to (intent?.action ?: "null")
            )
        )

172
        components.publicSuffixList.prefetch()
173

174
        setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent))
175
        setContentView(R.layout.activity_home)
176
177
178

        // Must be after we set the content view
        if (isVisuallyComplete) {
179
180
            components.performance.visualCompletenessQueue
                .attachViewToRunVisualCompletenessQueueLater(WeakReference(rootContainer))
181
182
        }

183
184
185
186
187
        privateNotificationObserver = PrivateNotificationFeature(
            applicationContext,
            components.core.store,
            PrivateNotificationService::class
        ).also {
188
189
            it.start()
        }
190

191
192
193
194
        if (isActivityColdStarted(
                intent,
                savedInstanceState
            ) && !externalSourceIntentProcessors.any {
195
196
197
198
199
200
                it.process(
                    intent,
                    navHost.navController,
                    this.intent
                )
            }
201
202
        ) {
            navigateToBrowserOnColdStart()
203
        }
204

205
        Performance.processIntentIfPerformanceTest(intent, this)
206

207
        if (settings().isTelemetryEnabled) {
208
209
210
211
212
213
            lifecycle.addObserver(
                BreadcrumbsRecorder(
                    components.analytics.crashReporter,
                    navHost.navController, ::getBreadcrumbMessage
                )
            )
214

215
216
            val safeIntent = intent?.toSafeIntent()
            safeIntent
217
218
                ?.let(::getIntentSource)
                ?.also { components.analytics.metrics.track(Event.OpenedApp(it)) }
219
220
221
            // record on cold startup
            safeIntent
                ?.let(::getIntentAllSource)
222
                ?.also { components.analytics.metrics.track(Event.AppReceivedIntent(it)) }
223
        }
224
        supportActionBar?.hide()
Gabriel Luong's avatar
Gabriel Luong committed
225

226
227
228
229
        lifecycle.addObservers(
            webExtensionPopupFeature,
            StartupTimeline.homeActivityLifecycleObserver
        )
230
231
232
233
234

        if (shouldAddToRecentsScreen(intent)) {
            intent.removeExtra(START_IN_RECENTS_SCREEN)
            moveTaskToBack(true)
        }
235
236

        captureSnapshotTelemetryMetrics()
237

238
        startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null)
239

240
        StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
241
    }
Jeff Boek's avatar
Jeff Boek committed
242

243
244
245
246
    protected open fun startupTelemetryOnCreateCalled(
        safeIntent: SafeIntent,
        hasSavedInstanceState: Boolean
    ) {
247
248
249
250
251
        components.appStartupTelemetry.onHomeActivityOnCreate(
            safeIntent,
            hasSavedInstanceState,
            homeActivityInitTimeStampNanoSeconds, rootContainer
        )
252
253
254
    }

    override fun onRestart() {
255
256
257
        // DO NOT MOVE ANYTHING ABOVE THIS..
        // we are measuring startup time for hot startup type
        startupTelemetryOnRestartCalled()
258
        super.onRestart()
259
    }
260

261
262
    private fun startupTelemetryOnRestartCalled() {
        components.appStartupTelemetry.onHomeActivityOnRestart(rootContainer)
263
264
    }

265
    @CallSuper
266
267
    override fun onResume() {
        super.onResume()
268

269
270
271
272
273
274
        // Diagnostic breadcrumb for "Display already aquired" crash:
        // https://github.com/mozilla-mobile/android-components/issues/7960
        breadcrumb(
            message = "onResume()"
        )

275
276
        components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
            lifecycleScope.launch {
277
                // Make sure accountManager is initialized.
278
                components.backgroundServices.accountManager.start()
279
                // If we're authenticated, kick-off a sync and a device state refresh.
280
                components.backgroundServices.accountManager.authenticatedAccount()?.let {
281
                    components.backgroundServices.accountManager.syncNow(
282
283
284
                        SyncReason.Startup,
                        debounce = true
                    )
285
                }
286
287
            }
        }
288
289
290

        // Launch this on a background thread so as not to affect startup performance
        lifecycleScope.launch(IO) {
Sawyer Blatz's avatar
Sawyer Blatz committed
291
292
            if (
                settings().isDefaultBrowser() &&
293
                settings().wasDefaultBrowserOnLastResume != settings().isDefaultBrowser()
Sawyer Blatz's avatar
Sawyer Blatz committed
294
            ) {
295
296
                metrics.track(Event.ChangedToDefaultBrowser)
            }
297
298

            settings().wasDefaultBrowserOnLastResume = settings().isDefaultBrowser()
299
        }
300
301
    }

302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
    override fun onStart() {
        super.onStart()

        // Diagnostic breadcrumb for "Display already aquired" crash:
        // https://github.com/mozilla-mobile/android-components/issues/7960
        breadcrumb(
            message = "onStart()"
        )
    }

    override fun onStop() {
        super.onStop()

        // Diagnostic breadcrumb for "Display already aquired" crash:
        // https://github.com/mozilla-mobile/android-components/issues/7960
        breadcrumb(
            message = "onStop()",
            data = mapOf(
                "finishing" to isFinishing.toString()
            )
        )
323
324

        components.appStartupTelemetry.onStop()
325
326
    }

327
    final override fun onPause() {
328
329
330
331
        // We should return to the browser if there were normal tabs when we left the app
        settings().shouldReturnToBrowser =
            components.core.store.state.getNormalOrPrivateTabs(private = false).isNotEmpty()

332
333
334
        if (settings().lastKnownMode.isPrivate) {
            window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
        }
335

336
337
338
339
        // We will remove this when AC code lands to emit a fact on getTopSites in DefaultTopSitesStorage
        // https://github.com/mozilla-mobile/android-components/issues/8679
        settings().topSitesSize = components.core.topSitesStorage.cachedTopSites.size

340
341
        super.onPause()

342
343
344
345
346
347
348
349
350
        // Diagnostic breadcrumb for "Display already aquired" crash:
        // https://github.com/mozilla-mobile/android-components/issues/7960
        breadcrumb(
            message = "onPause()",
            data = mapOf(
                "finishing" to isFinishing.toString()
            )
        )

351
352
353
354
355
356
357
        // Every time the application goes into the background, it is possible that the user
        // is about to change the browsers installed on their system. Therefore, we reset the cache of
        // all the installed browsers.
        //
        // NB: There are ways for the user to install new products without leaving the browser.
        BrowsersCache.resetAll()
    }
358

359
360
    override fun onDestroy() {
        super.onDestroy()
361
362
363
364
365
366
367
368
369
370

        // Diagnostic breadcrumb for "Display already aquired" crash:
        // https://github.com/mozilla-mobile/android-components/issues/7960
        breadcrumb(
            message = "onDestroy()",
            data = mapOf(
                "finishing" to isFinishing.toString()
            )
        )

371
372
373
        privateNotificationObserver?.stop()
    }

374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)

        // Diagnostic breadcrumb for "Display already aquired" crash:
        // https://github.com/mozilla-mobile/android-components/issues/7960
        breadcrumb(
            message = "onConfigurationChanged()"
        )
    }

    override fun recreate() {
        // Diagnostic breadcrumb for "Display already aquired" crash:
        // https://github.com/mozilla-mobile/android-components/issues/7960
        breadcrumb(
            message = "recreate()"
        )

        super.recreate()
    }

394
395
396
397
398
    /**
     * Handles intents received when the activity is open.
     */
    final override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
399
400
401
402
        intent?.let {
            handleNewIntent(it)
        }
    }
403

404
    open fun handleNewIntent(intent: Intent) {
405
406
407
408
409
410
411
412
413
        // Diagnostic breadcrumb for "Display already aquired" crash:
        // https://github.com/mozilla-mobile/android-components/issues/7960
        breadcrumb(
            message = "onNewIntent()",
            data = mapOf(
                "intent" to intent.action.toString()
            )
        )

414
415
416
417
        val intentProcessors =
            listOf(CrashReporterIntentProcessor()) + externalSourceIntentProcessors
        val intentHandled =
            intentProcessors.any { it.process(intent, navHost.navController, this.intent) }
418
        browsingModeManager.mode = getModeFromIntentOrLastKnown(intent)
419
420
421
422
423
424
425
426
427
428

        if (intentHandled) {
            supportFragmentManager
                .primaryNavigationFragment
                ?.childFragmentManager
                ?.fragments
                ?.lastOrNull()
                ?.let { it as? TabTrayDialogFragment }
                ?.also { it.dismissAllowingStateLoss() }
        }
429
430
431
432
433
434
435

        // Note: This does not work in case of an user sending an intent with ACTION_VIEW
        // for example, launch the application, and than use adb to send an intent with
        // ACTION_VIEW to open a link. In this case, we will get multiple telemetry events.
        intent
            .toSafeIntent()
            .let(::getIntentAllSource)
436
437
            ?.also { components.analytics.metrics.track(Event.AppReceivedIntent(it)) }

438
        components.appStartupTelemetry.onHomeActivityOnNewIntent(intent.toSafeIntent())
439
440
441
442
443
444
    }

    /**
     * Overrides view inflation to inject a custom [EngineView] from [components].
     */
    final override fun onCreateView(
445
446
447
448
        parent: View?,
        name: String,
        context: Context,
        attrs: AttributeSet
449
    ): View? = when (name) {
450
451
        EngineView::class.java.name -> components.core.engine.createView(context, attrs).apply {
            selectionActionDelegate = DefaultSelectionActionDelegate(
452
453
454
455
                BrowserStoreSearchAdapter(
                    components.core.store,
                    tabId = getIntentSessionId(intent.toSafeIntent())
                ),
Sawyer Blatz's avatar
Sawyer Blatz committed
456
457
458
459
460
461
                resources = context.resources,
                shareTextClicked = { share(it) },
                emailTextClicked = { email(it) },
                callTextClicked = { call(it) },
                actionSorter = ::actionSorter
            )
462
        }.asView()
463
464
        else -> super.onCreateView(parent, name, context, attrs)
    }
465

466
467
468
    @Suppress("MagicNumber")
    // Defining the positions as constants doesn't seem super useful here.
    private fun actionSorter(actions: Array<String>): Array<String> {
Sawyer Blatz's avatar
Sawyer Blatz committed
469
470
        val order = hashMapOf<String, Int>()

471
472
473
474
        order["CUSTOM_CONTEXT_MENU_EMAIL"] = 0
        order["CUSTOM_CONTEXT_MENU_CALL"] = 1
        order["org.mozilla.geckoview.COPY"] = 2
        order["CUSTOM_CONTEXT_MENU_SEARCH"] = 3
475
        order["CUSTOM_CONTEXT_MENU_SEARCH_PRIVATELY"] = 4
476
477
478
        order["org.mozilla.geckoview.PASTE"] = 5
        order["org.mozilla.geckoview.SELECT_ALL"] = 6
        order["CUSTOM_CONTEXT_MENU_SHARE"] = 7
Sawyer Blatz's avatar
Sawyer Blatz committed
479
480
481
482
483
484
485

        return actions.sortedBy { actionName ->
            // Sort the actions in our preferred order, putting "other" actions unsorted at the end
            order[actionName] ?: actions.size
        }.toTypedArray()
    }

486
    final override fun onBackPressed() {
487
        supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
488
            if (it is UserInteractionHandler && it.onBackPressed()) {
489
490
491
492
493
                return
            }
        }
        super.onBackPressed()
    }
494

495
496
497
498
499
500
501
502
    private fun shouldUseCustomBackLongPress(): Boolean {
        val isAndroidN =
            Build.VERSION.SDK_INT == Build.VERSION_CODES.N || Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1
        // Huawei devices seem to have problems with onKeyLongPress
        // See https://github.com/mozilla-mobile/fenix/issues/13498
        val isHuawei = Build.MANUFACTURER.equals("huawei", ignoreCase = true)
        return isAndroidN || isHuawei
    }
503
504
505
506
507
508
509
510
511
512
513
514

    private fun handleBackLongPress(): Boolean {
        supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
            if (it is OnBackLongPressedListener && it.onBackLongPressed()) {
                return true
            }
        }
        return false
    }

    final override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        // Inspired by https://searchfox.org/mozilla-esr68/source/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java#584-613
515
        // Android N and Huawei devices have broken onKeyLongPress events for the back button, so we
516
517
518
519
        // instead implement the long press behavior ourselves
        // - For short presses, we cancel the callback in onKeyUp
        // - For long presses, the normal keypress is marked as cancelled, hence won't be handled elsewhere
        //   (but Android still provides the haptic feedback), and the long press action is run
520
        if (shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
521
522
523
524
525
526
527
528
529
            backLongPressJob = lifecycleScope.launch {
                delay(ViewConfiguration.getLongPressTimeout().toLong())
                handleBackLongPress()
            }
        }
        return super.onKeyDown(keyCode, event)
    }

    final override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
530
        if (shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
531
532
533
534
535
536
537
538
            backLongPressJob?.cancel()
        }
        return super.onKeyUp(keyCode, event)
    }

    final override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean {
        // onKeyLongPress is broken in Android N so we don't handle back button long presses here
        // for N. The version check ensures we don't handle back button long presses twice.
539
        if (!shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
540
541
542
543
544
            return handleBackLongPress()
        }
        return super.onKeyLongPress(keyCode, event)
    }

545
546
547
548
549
550
551
552
553
554
    final override fun onUserLeaveHint() {
        supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
            if (it is UserInteractionHandler && it.onHomePressed()) {
                return
            }
        }

        super.onUserLeaveHint()
    }

555
    protected open fun getBreadcrumbMessage(destination: NavDestination): String {
556
557
558
559
560
561
562
563
564
565
566
567
568
        val fragmentName = resources.getResourceEntryName(destination.id)
        return "Changing to fragment $fragmentName, isCustomTab: false"
    }

    @VisibleForTesting(otherwise = PROTECTED)
    internal open fun getIntentSource(intent: SafeIntent): Event.OpenedApp.Source? {
        return when {
            intent.isLauncherIntent -> Event.OpenedApp.Source.APP_ICON
            intent.action == Intent.ACTION_VIEW -> Event.OpenedApp.Source.LINK
            else -> null
        }
    }

569
    protected open fun getIntentAllSource(intent: SafeIntent): Event.AppReceivedIntent.Source? {
570
        return when {
571
572
573
            intent.isLauncherIntent -> Event.AppReceivedIntent.Source.APP_ICON
            intent.action == Intent.ACTION_VIEW -> Event.AppReceivedIntent.Source.LINK
            else -> Event.AppReceivedIntent.Source.UNKNOWN
574
575
576
        }
    }

577
578
    /**
     * External sources such as 3rd party links and shortcuts use this function to enter
579
580
     * private mode directly before the content view is created. Returns the mode set by the intent
     * otherwise falls back to the last known mode.
581
     */
582
    internal fun getModeFromIntentOrLastKnown(intent: Intent?): BrowsingMode {
583
584
585
        intent?.toSafeIntent()?.let {
            if (it.hasExtra(PRIVATE_BROWSING_MODE)) {
                val startPrivateMode = it.getBooleanExtra(PRIVATE_BROWSING_MODE, false)
586
                return BrowsingMode.fromBoolean(isPrivate = startPrivateMode)
587
588
            }
        }
589
        return settings().lastKnownMode
590
591
    }

592
593
594
595
596
597
598
599
600
601
602
603
604
    /**
     * Determines whether the activity should be pushed to be backstack (i.e., 'minimized' to the recents
     * screen) upon starting.
     * @param intent - The intent that started this activity. Is checked for having the 'START_IN_RECENTS_SCREEN'-extra.
     * @return true if the activity should be started and pushed to the recents screen, false otherwise.
     */
    private fun shouldAddToRecentsScreen(intent: Intent?): Boolean {
        intent?.toSafeIntent()?.let {
            return it.getBooleanExtra(START_IN_RECENTS_SCREEN, false)
        }
        return false
    }

605
    private fun setupThemeAndBrowsingMode(mode: BrowsingMode) {
606
        settings().lastKnownMode = mode
607
        browsingModeManager = createBrowsingModeManager(mode)
608
609
610
611
612
        themeManager = createThemeManager()
        themeManager.setActivityTheme(this)
        themeManager.applyStatusBarTheme(this)
    }

613
614
615
616
    /**
     * Returns the [supportActionBar], inflating it if necessary.
     * Everyone should call this instead of supportActionBar.
     */
617
    override fun getSupportActionBarAndInflateIfNecessary(): ActionBar {
618
        if (!isToolbarInflated) {
619
            navigationToolbar = navigationToolbarStub.inflate() as Toolbar
620
621

            setSupportActionBar(navigationToolbar)
622
            // Add ids to this that we don't want to have a toolbar back button
623
            setupNavigationToolbar()
624

625
626
627
            isToolbarInflated = true
        }
        return supportActionBar!!
628
629
    }

630
631
632
633
634
635
636
637
638
639
640
641
642
    @Suppress("SpreadOperator")
    fun setupNavigationToolbar(vararg topLevelDestinationIds: Int) {
        NavigationUI.setupWithNavController(
            navigationToolbar,
            navHost.navController,
            AppBarConfiguration.Builder(*topLevelDestinationIds).build()
        )

        navigationToolbar.setNavigationOnClickListener {
            onBackPressed()
        }
    }

643
644
    protected open fun getIntentSessionId(intent: SafeIntent): String? = null

Sebastian Kaspari's avatar
Sebastian Kaspari committed
645
646
647
648
649
650
    /**
     * Navigates to the browser fragment and loads a URL or performs a search (depending on the
     * value of [searchTermOrURL]).
     *
     * @param flags Flags that will be used when loading the URL (not applied to searches).
     */
651
    @Suppress("LongParameterList")
652
    fun openToBrowserAndLoad(
653
        searchTermOrURL: String,
654
655
        newTab: Boolean,
        from: BrowserDirection,
656
        customTabSessionId: String? = null,
657
        engine: SearchEngine? = null,
Sebastian Kaspari's avatar
Sebastian Kaspari committed
658
659
        forceSearch: Boolean = false,
        flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none()
660
    ) {
661
        openToBrowser(from, customTabSessionId)
Sebastian Kaspari's avatar
Sebastian Kaspari committed
662
        load(searchTermOrURL, newTab, engine, forceSearch, flags)
663
664
    }

665
    fun openToBrowser(from: BrowserDirection, customTabSessionId: String? = null) {
666
667
        if (navHost.navController.alreadyOnDestination(R.id.browserFragment)) return
        @IdRes val fragmentId = if (from.fragmentId != 0) from.fragmentId else null
Tiger Oakes's avatar
Tiger Oakes committed
668
        val directions = getNavDirections(from, customTabSessionId)
669
670
671
        if (directions != null) {
            navHost.navController.nav(fragmentId, directions)
        }
672
673
    }

Tiger Oakes's avatar
Tiger Oakes committed
674
675
676
    protected open fun getNavDirections(
        from: BrowserDirection,
        customTabSessionId: String?
677
    ): NavDirections? = when (from) {
Tiger Oakes's avatar
Tiger Oakes committed
678
679
680
        BrowserDirection.FromGlobal ->
            NavGraphDirections.actionGlobalBrowser(customTabSessionId)
        BrowserDirection.FromHome ->
681
            HomeFragmentDirections.actionGlobalBrowser(customTabSessionId)
682
683
        BrowserDirection.FromSearchDialog ->
            SearchDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
Tiger Oakes's avatar
Tiger Oakes committed
684
        BrowserDirection.FromSettings ->
Jeff Boek's avatar
Jeff Boek committed
685
            SettingsFragmentDirections.actionGlobalBrowser(customTabSessionId)
686
687
        BrowserDirection.FromSyncedTabs ->
            SyncedTabsFragmentDirections.actionGlobalBrowser(customTabSessionId)
Tiger Oakes's avatar
Tiger Oakes committed
688
        BrowserDirection.FromBookmarks ->
Jeff Boek's avatar
Jeff Boek committed
689
            BookmarkFragmentDirections.actionGlobalBrowser(customTabSessionId)
Tiger Oakes's avatar
Tiger Oakes committed
690
        BrowserDirection.FromHistory ->
Jeff Boek's avatar
Jeff Boek committed
691
            HistoryFragmentDirections.actionGlobalBrowser(customTabSessionId)
692
693
        BrowserDirection.FromTrackingProtectionExceptions ->
            TrackingProtectionExceptionsFragmentDirections.actionGlobalBrowser(customTabSessionId)
694
        BrowserDirection.FromAbout ->
Jeff Boek's avatar
Jeff Boek committed
695
            AboutFragmentDirections.actionGlobalBrowser(customTabSessionId)
696
        BrowserDirection.FromTrackingProtection ->
Jeff Boek's avatar
Jeff Boek committed
697
            TrackingProtectionFragmentDirections.actionGlobalBrowser(customTabSessionId)
698
        BrowserDirection.FromSavedLoginsFragment ->
699
            SavedLoginsAuthFragmentDirections.actionGlobalBrowser(customTabSessionId)
700
701
702
703
704
705
        BrowserDirection.FromAddNewDeviceFragment ->
            AddNewDeviceFragmentDirections.actionGlobalBrowser(customTabSessionId)
        BrowserDirection.FromAddSearchEngineFragment ->
            AddSearchEngineFragmentDirections.actionGlobalBrowser(customTabSessionId)
        BrowserDirection.FromEditCustomSearchEngineFragment ->
            EditCustomSearchEngineFragmentDirections.actionGlobalBrowser(customTabSessionId)
706
        BrowserDirection.FromAddonDetailsFragment ->
707
            AddonDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
708
709
        BrowserDirection.FromAddonPermissionsDetailsFragment ->
            AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
710
711
        BrowserDirection.FromLoginDetailFragment ->
            LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
712
713
        BrowserDirection.FromTabTray ->
            TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
ekager's avatar
ekager committed
714
715
        BrowserDirection.FromRecentlyClosed ->
            RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId)
Tiger Oakes's avatar
Tiger Oakes committed
716
717
    }

Sebastian Kaspari's avatar
Sebastian Kaspari committed
718
719
720
721
722
    /**
     * Loads a URL or performs a search (depending on the value of [searchTermOrURL]).
     *
     * @param flags Flags that will be used when loading the URL (not applied to searches).
     */
723
724
725
726
    private fun load(
        searchTermOrURL: String,
        newTab: Boolean,
        engine: SearchEngine?,
Sebastian Kaspari's avatar
Sebastian Kaspari committed
727
728
        forceSearch: Boolean,
        flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none()
729
    ) {
730
        val startTime = components.core.engine.profiler?.getProfilerTime()
731
        val mode = browsingModeManager.mode
732

733
        val loadUrlUseCase = if (newTab) {
734
735
736
            when (mode) {
                BrowsingMode.Private -> components.useCases.tabsUseCases.addPrivateTab
                BrowsingMode.Normal -> components.useCases.tabsUseCases.addTab
737
738
739
740
            }
        } else components.useCases.sessionUseCases.loadUrl

        val searchUseCase: (String) -> Unit = { searchTerms ->
741
            if (newTab) {
742
                components.useCases.searchUseCases.newTabSearch
743
744
                    .invoke(
                        searchTerms,
745
                        SessionState.Source.USER_ENTERED,
746
                        true,
747
                        mode.isPrivate,
748
749
                        searchEngine = engine
                    )
750
            } else components.useCases.searchUseCases.defaultSearch.invoke(searchTerms, engine)
751
752
        }

753
        if (!forceSearch && searchTermOrURL.isUrl()) {
Sebastian Kaspari's avatar
Sebastian Kaspari committed
754
            loadUrlUseCase.invoke(searchTermOrURL.toNormalizedUrl(), flags)
755
        } else {
756
            searchUseCase.invoke(searchTermOrURL)
757
        }
758
759
760
761

        if (components.core.engine.profiler?.isProfilerActive() == true) {
            // Wrapping the `addMarker` method with `isProfilerActive` even though it's no-op when
            // profiler is not active. That way, `text` argument will not create a string builder all the time.
762
763
764
765
766
            components.core.engine.profiler?.addMarker(
                "HomeActivity.load",
                startTime,
                "newTab: $newTab"
            )
767
        }
768
769
    }

770
771
772
    open fun navigateToBrowserOnColdStart() {
        // Normal tabs + cold start -> Should go back to browser if we had any tabs open when we left last
        // except for PBM + Cold Start there won't be any tabs since they're evicted so we never will navigate
773
        if (settings().shouldReturnToBrowser &&
774
775
            !browsingModeManager.mode.isPrivate
        ) {
776
777
778
779
            openToBrowser(BrowserDirection.FromGlobal, null)
        }
    }

780
    override fun attachBaseContext(base: Context) {
781
        base.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
782
783
784
785
            super.attachBaseContext(base)
        }
    }

786
    protected open fun createBrowsingModeManager(initialMode: BrowsingMode): BrowsingModeManager {
787
        return DefaultBrowsingModeManager(initialMode, components.settings) { newMode ->
788
789
790
791
            themeManager.currentTheme = newMode
        }
    }

792
793
    protected open fun createThemeManager(): ThemeManager {
        return DefaultThemeManager(browsingModeManager.mode, this)
Jeff Boek's avatar
Jeff Boek committed
794
795
    }

Gabriel Luong's avatar
Gabriel Luong committed
796
797
798
799
800
801
802
803
    private fun openPopup(webExtensionState: WebExtensionState) {
        val action = NavGraphDirections.actionGlobalWebExtensionActionPopupFragment(
            webExtensionId = webExtensionState.id,
            webExtensionTitle = webExtensionState.name
        )
        navHost.navController.navigate(action)
    }

804
805
806
807
    /**
     * The root container is null at this point, so let the HomeActivity know that
     * we are visually complete.
     */
808
    fun setVisualCompletenessQueueReady() {
809
810
811
        isVisuallyComplete = true
    }

812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
    private fun captureSnapshotTelemetryMetrics() = CoroutineScope(Dispatchers.IO).launch {
        // PWA
        val recentlyUsedPwaCount = components.core.webAppShortcutManager.recentlyUsedWebAppsCount(
            activeThresholdMs = PWA_RECENTLY_USED_THRESHOLD
        )
        if (recentlyUsedPwaCount == 0) {
            Metrics.hasRecentPwas.set(false)
        } else {
            Metrics.hasRecentPwas.set(true)
            // This metric's lifecycle is set to 'application', meaning that it gets reset upon
            // application restart. Combined with the behaviour of the metric type itself (a growing counter),
            // it's important that this metric is only set once per application's lifetime.
            // Otherwise, we're going to over-count.
            Metrics.recentlyUsedPwaCount.add(recentlyUsedPwaCount)
        }
    }

829
    @VisibleForTesting
830
    internal fun isActivityColdStarted(startingIntent: Intent, activityIcicle: Bundle?): Boolean {
831
832
        // First time opening this activity in the task.
        // Cold start / start from Recents after back press.
833
834
835
836
837
        return activityIcicle == null &&
                // Activity was restarted from Recents after it was destroyed by Android while in background
                // in cases of memory pressure / "Don't keep activities".
                startingIntent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == 0
    }
838

839
840
    companion object {
        const val OPEN_TO_BROWSER = "open_to_browser"
Yeon Taek Jeong's avatar
Yeon Taek Jeong committed
841
842
        const val OPEN_TO_BROWSER_AND_LOAD = "open_to_browser_and_load"
        const val OPEN_TO_SEARCH = "open_to_search"
843
        const val PRIVATE_BROWSING_MODE = "private_browsing_mode"
844
845
        const val EXTRA_DELETE_PRIVATE_TABS = "notification_delete_and_open"
        const val EXTRA_OPENED_FROM_NOTIFICATION = "notification_open"
846
        const val START_IN_RECENTS_SCREEN = "start_in_recents_screen"
847
848
849
850

        // PWA must have been used within last 30 days to be considered "recently used" for the
        // telemetry purposes.
        const val PWA_RECENTLY_USED_THRESHOLD = DateUtils.DAY_IN_MILLIS * 30L
851
    }
Jeff Boek's avatar
Jeff Boek committed
852
}