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

107
108
109
110
111
112
/**
 * 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
 */
113
@OptIn(ExperimentalCoroutinesApi::class)
114
@SuppressWarnings("TooManyFunctions", "LargeClass")
115
open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
116
117
118
119
120
    // 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()
121

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

126
127
    private var isVisuallyComplete = false

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

131
132
    private var isToolbarInflated = false

133
134
    private var isBeingRecreated = false

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

139
140
141
142
    private val navHost by lazy {
        supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment
    }

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

153
154
155
    // See onKeyDown for why this is necessary
    private var backLongPressJob: Job? = null

156
157
    private lateinit var navigationToolbar: Toolbar

158
    final override fun onCreate(savedInstanceState: Bundle?) {
159
        components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager)
160

161
        // There is disk read violations on some devices such as samsung and pixel for android 9/10
162
        components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
163
164
            super.onCreate(savedInstanceState)
        }
165

166
167
168
169
170
171
172
173
174
175
        // 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")
            )
        )

176
        components.publicSuffixList.prefetch()
177

178
        setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent))
179
        setContentView(R.layout.activity_home)
180
181
182

        // Must be after we set the content view
        if (isVisuallyComplete) {
183
184
            components.performance.visualCompletenessQueue
                .attachViewToRunVisualCompletenessQueueLater(WeakReference(rootContainer))
185
186
        }

187
        checkPrivateShortcutEntryPoint(intent)
188
189
190
191
192
        privateNotificationObserver = PrivateNotificationFeature(
            applicationContext,
            components.core.store,
            PrivateNotificationService::class
        ).also {
193
194
            it.start()
        }
195

196
197
198
199
        if (isActivityColdStarted(
                intent,
                savedInstanceState
            ) && !externalSourceIntentProcessors.any {
200
201
202
203
204
205
                it.process(
                    intent,
                    navHost.navController,
                    this.intent
                )
            }
206
207
        ) {
            navigateToBrowserOnColdStart()
208
        }
209

210
        Performance.processIntentIfPerformanceTest(intent, this)
211

212
        if (settings().isTelemetryEnabled) {
213
214
215
216
217
218
            lifecycle.addObserver(
                BreadcrumbsRecorder(
                    components.analytics.crashReporter,
                    navHost.navController, ::getBreadcrumbMessage
                )
            )
219

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

231
232
233
234
        lifecycle.addObservers(
            webExtensionPopupFeature,
            StartupTimeline.homeActivityLifecycleObserver
        )
235
236
237
238
239

        if (shouldAddToRecentsScreen(intent)) {
            intent.removeExtra(START_IN_RECENTS_SCREEN)
            moveTaskToBack(true)
        }
240
241

        captureSnapshotTelemetryMetrics()
242

243
        startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null)
244

245
        StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
246
    }
Jeff Boek's avatar
Jeff Boek committed
247

248
249
250
251
    protected open fun startupTelemetryOnCreateCalled(
        safeIntent: SafeIntent,
        hasSavedInstanceState: Boolean
    ) {
252
253
254
255
256
        components.appStartupTelemetry.onHomeActivityOnCreate(
            safeIntent,
            hasSavedInstanceState,
            homeActivityInitTimeStampNanoSeconds, rootContainer
        )
257
258
259
    }

    override fun onRestart() {
260
261
262
        // DO NOT MOVE ANYTHING ABOVE THIS..
        // we are measuring startup time for hot startup type
        startupTelemetryOnRestartCalled()
263
        super.onRestart()
264
    }
265

266
267
    private fun startupTelemetryOnRestartCalled() {
        components.appStartupTelemetry.onHomeActivityOnRestart(rootContainer)
268
269
    }

270
    @CallSuper
271
272
    override fun onResume() {
        super.onResume()
273

274
275
276
277
278
279
        // Diagnostic breadcrumb for "Display already aquired" crash:
        // https://github.com/mozilla-mobile/android-components/issues/7960
        breadcrumb(
            message = "onResume()"
        )

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

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

            settings().wasDefaultBrowserOnLastResume = settings().isDefaultBrowser()
304
        }
305
306
    }

307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
    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()
            )
        )
328
329

        components.appStartupTelemetry.onStop()
330
331
    }

332
    final override fun onPause() {
333
334
335
336
        // 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()

337
338
339
        if (settings().lastKnownMode.isPrivate) {
            window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
        }
340

341
342
343
344
        // 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

345
346
        super.onPause()

347
348
349
350
351
352
353
354
355
        // 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()
            )
        )

356
357
358
359
360
361
362
        // 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()
    }
363

364
365
    override fun onDestroy() {
        super.onDestroy()
366
367
368
369
370
371
372
373
374
375

        // 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()
            )
        )

376
        privateNotificationObserver?.stop()
377
378
379
380
381
382
383
384
        if (!isBeingRecreated && !(application as FenixApplication).isTerminating()) {
            // We assume the Activity is being destroyed because the user
            // swiped away the app on the Recent screen. When this happens,
            // we assume the user expects the entire Application is destroyed
            // and not only the top Activity/Task. Therefore we kill the
            // underlying Application, as well.
            (application as FenixApplication).terminate()
        }
385
386
    }

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

404
405
        isBeingRecreated = true

406
407
408
        super.recreate()
    }

409
410
411
412
413
    /**
     * Handles intents received when the activity is open.
     */
    final override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
414
415
        intent ?: return

416
417
418
419
420
421
422
423
424
        // 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()
            )
        )

425
426
427
428
        val intentProcessors =
            listOf(CrashReporterIntentProcessor()) + externalSourceIntentProcessors
        val intentHandled =
            intentProcessors.any { it.process(intent, navHost.navController, this.intent) }
429
        browsingModeManager.mode = getModeFromIntentOrLastKnown(intent)
430
431
432
433
434
435
436
437
438
439

        if (intentHandled) {
            supportFragmentManager
                .primaryNavigationFragment
                ?.childFragmentManager
                ?.fragments
                ?.lastOrNull()
                ?.let { it as? TabTrayDialogFragment }
                ?.also { it.dismissAllowingStateLoss() }
        }
440
441
442
443
444
445
446

        // 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)
447
448
            ?.also { components.analytics.metrics.track(Event.AppReceivedIntent(it)) }

449
        components.appStartupTelemetry.onHomeActivityOnNewIntent(intent.toSafeIntent())
450
451
452
453
454
455
    }

    /**
     * Overrides view inflation to inject a custom [EngineView] from [components].
     */
    final override fun onCreateView(
456
457
458
459
        parent: View?,
        name: String,
        context: Context,
        attrs: AttributeSet
460
    ): View? = when (name) {
461
462
        EngineView::class.java.name -> components.core.engine.createView(context, attrs).apply {
            selectionActionDelegate = DefaultSelectionActionDelegate(
463
464
465
466
                BrowserStoreSearchAdapter(
                    components.core.store,
                    tabId = getIntentSessionId(intent.toSafeIntent())
                ),
Sawyer Blatz's avatar
Sawyer Blatz committed
467
468
469
470
471
472
                resources = context.resources,
                shareTextClicked = { share(it) },
                emailTextClicked = { email(it) },
                callTextClicked = { call(it) },
                actionSorter = ::actionSorter
            )
473
        }.asView()
474
475
        else -> super.onCreateView(parent, name, context, attrs)
    }
476

477
478
479
    @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
480
481
        val order = hashMapOf<String, Int>()

482
483
484
485
        order["CUSTOM_CONTEXT_MENU_EMAIL"] = 0
        order["CUSTOM_CONTEXT_MENU_CALL"] = 1
        order["org.mozilla.geckoview.COPY"] = 2
        order["CUSTOM_CONTEXT_MENU_SEARCH"] = 3
486
        order["CUSTOM_CONTEXT_MENU_SEARCH_PRIVATELY"] = 4
487
488
489
        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
490
491
492
493
494
495
496

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

497
    final override fun onBackPressed() {
498
        supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
499
            if (it is UserInteractionHandler && it.onBackPressed()) {
500
501
502
503
504
                return
            }
        }
        super.onBackPressed()
    }
505

506
507
508
509
510
511
512
513
    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
    }
514
515
516
517
518
519
520
521
522
523
524
525

    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
526
        // Android N and Huawei devices have broken onKeyLongPress events for the back button, so we
527
528
529
530
        // 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
531
        if (shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
532
533
534
535
536
537
538
539
540
            backLongPressJob = lifecycleScope.launch {
                delay(ViewConfiguration.getLongPressTimeout().toLong())
                handleBackLongPress()
            }
        }
        return super.onKeyDown(keyCode, event)
    }

    final override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
541
        if (shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
542
543
544
545
546
547
548
549
            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.
550
        if (!shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
551
552
553
554
555
            return handleBackLongPress()
        }
        return super.onKeyLongPress(keyCode, event)
    }

556
557
558
559
560
561
562
563
564
565
    final override fun onUserLeaveHint() {
        supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
            if (it is UserInteractionHandler && it.onHomePressed()) {
                return
            }
        }

        super.onUserLeaveHint()
    }

566
    protected open fun getBreadcrumbMessage(destination: NavDestination): String {
567
568
569
570
571
572
573
574
575
576
577
578
579
        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
        }
    }

580
    protected open fun getIntentAllSource(intent: SafeIntent): Event.AppReceivedIntent.Source? {
581
        return when {
582
583
584
            intent.isLauncherIntent -> Event.AppReceivedIntent.Source.APP_ICON
            intent.action == Intent.ACTION_VIEW -> Event.AppReceivedIntent.Source.LINK
            else -> Event.AppReceivedIntent.Source.UNKNOWN
585
586
587
        }
    }

588
589
    /**
     * External sources such as 3rd party links and shortcuts use this function to enter
590
591
     * private mode directly before the content view is created. Returns the mode set by the intent
     * otherwise falls back to the last known mode.
592
     */
593
    internal fun getModeFromIntentOrLastKnown(intent: Intent?): BrowsingMode {
594
595
        intent?.toSafeIntent()?.let {
            if (it.hasExtra(PRIVATE_BROWSING_MODE)) {
596
597
598
                val startPrivateMode = settings().shouldDisableNormalMode ||
                    it.getBooleanExtra(PRIVATE_BROWSING_MODE, settings().openLinksInAPrivateTab)

599
                return BrowsingMode.fromBoolean(isPrivate = startPrivateMode)
600
601
            }
        }
602
603
604
605
606
        return when {
            settings().shouldDisableNormalMode -> BrowsingMode.Private
            settings().openLinksInAPrivateTab -> BrowsingMode.Private
            else -> settings().lastKnownMode
        }
607
608
    }

609
610
611
612
613
614
615
616
617
618
619
620
621
622
    /**
     * 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
    }

    private fun checkPrivateShortcutEntryPoint(intent: Intent) {
623
624
        val shouldStartPrivate = settings().shouldDisableNormalMode ||
                    intent.getStringExtra(OPEN_TO_SEARCH) ==
625
626
                    StartSearchIntentProcessor.STATIC_SHORTCUT_NEW_PRIVATE_TAB ||
                    intent.getStringExtra(OPEN_TO_SEARCH) ==
627
628
                    StartSearchIntentProcessor.PRIVATE_BROWSING_PINNED_SHORTCUT
        if (intent.hasExtra(OPEN_TO_SEARCH) && shouldStartPrivate) {
629
            PrivateNotificationService.isStartedFromPrivateShortcut = true
630
631
632
        }
    }

633
    private fun setupThemeAndBrowsingMode(mode: BrowsingMode) {
634
        settings().lastKnownMode = mode
635
        browsingModeManager = createBrowsingModeManager(mode)
636
637
638
639
640
        themeManager = createThemeManager()
        themeManager.setActivityTheme(this)
        themeManager.applyStatusBarTheme(this)
    }

641
642
643
644
    /**
     * Returns the [supportActionBar], inflating it if necessary.
     * Everyone should call this instead of supportActionBar.
     */
645
    override fun getSupportActionBarAndInflateIfNecessary(): ActionBar {
646
        if (!isToolbarInflated) {
647
            navigationToolbar = navigationToolbarStub.inflate() as Toolbar
648
649

            setSupportActionBar(navigationToolbar)
650
            // Add ids to this that we don't want to have a toolbar back button
651
            setupNavigationToolbar()
652

653
654
655
            isToolbarInflated = true
        }
        return supportActionBar!!
656
657
    }

658
659
660
661
662
663
664
665
666
667
668
669
670
    @Suppress("SpreadOperator")
    fun setupNavigationToolbar(vararg topLevelDestinationIds: Int) {
        NavigationUI.setupWithNavController(
            navigationToolbar,
            navHost.navController,
            AppBarConfiguration.Builder(*topLevelDestinationIds).build()
        )

        navigationToolbar.setNavigationOnClickListener {
            onBackPressed()
        }
    }

671
672
    protected open fun getIntentSessionId(intent: SafeIntent): String? = null

Sebastian Kaspari's avatar
Sebastian Kaspari committed
673
674
675
676
677
678
    /**
     * 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).
     */
679
    @Suppress("LongParameterList")
680
    fun openToBrowserAndLoad(
681
        searchTermOrURL: String,
682
683
        newTab: Boolean,
        from: BrowserDirection,
684
        customTabSessionId: String? = null,
685
        engine: SearchEngine? = null,
Sebastian Kaspari's avatar
Sebastian Kaspari committed
686
687
        forceSearch: Boolean = false,
        flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none()
688
    ) {
689
        openToBrowser(from, customTabSessionId)
Sebastian Kaspari's avatar
Sebastian Kaspari committed
690
        load(searchTermOrURL, newTab, engine, forceSearch, flags)
691
692
    }

693
    fun openToBrowser(from: BrowserDirection, customTabSessionId: String? = null) {
694
695
        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
696
        val directions = getNavDirections(from, customTabSessionId)
697
698
699
        if (directions != null) {
            navHost.navController.nav(fragmentId, directions)
        }
700
701
    }

Tiger Oakes's avatar
Tiger Oakes committed
702
703
704
    protected open fun getNavDirections(
        from: BrowserDirection,
        customTabSessionId: String?
705
    ): NavDirections? = when (from) {
Tiger Oakes's avatar
Tiger Oakes committed
706
707
708
        BrowserDirection.FromGlobal ->
            NavGraphDirections.actionGlobalBrowser(customTabSessionId)
        BrowserDirection.FromHome ->
709
            HomeFragmentDirections.actionGlobalBrowser(customTabSessionId)
710
711
        BrowserDirection.FromSearchDialog ->
            SearchDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
Tiger Oakes's avatar
Tiger Oakes committed
712
        BrowserDirection.FromSettings ->
Jeff Boek's avatar
Jeff Boek committed
713
            SettingsFragmentDirections.actionGlobalBrowser(customTabSessionId)
714
715
        BrowserDirection.FromSyncedTabs ->
            SyncedTabsFragmentDirections.actionGlobalBrowser(customTabSessionId)
Tiger Oakes's avatar
Tiger Oakes committed
716
        BrowserDirection.FromBookmarks ->
Jeff Boek's avatar
Jeff Boek committed
717
            BookmarkFragmentDirections.actionGlobalBrowser(customTabSessionId)
Tiger Oakes's avatar
Tiger Oakes committed
718
        BrowserDirection.FromHistory ->
Jeff Boek's avatar
Jeff Boek committed
719
            HistoryFragmentDirections.actionGlobalBrowser(customTabSessionId)
720
721
        BrowserDirection.FromTrackingProtectionExceptions ->
            TrackingProtectionExceptionsFragmentDirections.actionGlobalBrowser(customTabSessionId)
722
        BrowserDirection.FromAbout ->
Jeff Boek's avatar
Jeff Boek committed
723
            AboutFragmentDirections.actionGlobalBrowser(customTabSessionId)
724
        BrowserDirection.FromTrackingProtection ->
Jeff Boek's avatar
Jeff Boek committed
725
            TrackingProtectionFragmentDirections.actionGlobalBrowser(customTabSessionId)
726
        BrowserDirection.FromSavedLoginsFragment ->
727
            SavedLoginsAuthFragmentDirections.actionGlobalBrowser(customTabSessionId)
728
729
730
731
732
733
        BrowserDirection.FromAddNewDeviceFragment ->
            AddNewDeviceFragmentDirections.actionGlobalBrowser(customTabSessionId)
        BrowserDirection.FromAddSearchEngineFragment ->
            AddSearchEngineFragmentDirections.actionGlobalBrowser(customTabSessionId)
        BrowserDirection.FromEditCustomSearchEngineFragment ->
            EditCustomSearchEngineFragmentDirections.actionGlobalBrowser(customTabSessionId)
734
        BrowserDirection.FromAddonDetailsFragment ->
735
            AddonDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
736
737
        BrowserDirection.FromAddonPermissionsDetailsFragment ->
            AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
738
739
        BrowserDirection.FromLoginDetailFragment ->
            LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
740
741
        BrowserDirection.FromTabTray ->
            TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
ekager's avatar
ekager committed
742
743
        BrowserDirection.FromRecentlyClosed ->
            RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId)
Tiger Oakes's avatar
Tiger Oakes committed
744
745
    }

Sebastian Kaspari's avatar
Sebastian Kaspari committed
746
747
748
749
750
    /**
     * 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).
     */
751
752
753
754
    private fun load(
        searchTermOrURL: String,
        newTab: Boolean,
        engine: SearchEngine?,
Sebastian Kaspari's avatar
Sebastian Kaspari committed
755
756
        forceSearch: Boolean,
        flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none()
757
    ) {
758
        val startTime = components.core.engine.profiler?.getProfilerTime()
759
        val mode = browsingModeManager.mode
760

761
        val loadUrlUseCase = if (newTab) {
762
763
764
            when (mode) {
                BrowsingMode.Private -> components.useCases.tabsUseCases.addPrivateTab
                BrowsingMode.Normal -> components.useCases.tabsUseCases.addTab
765
766
767
768
            }
        } else components.useCases.sessionUseCases.loadUrl

        val searchUseCase: (String) -> Unit = { searchTerms ->
769
            if (newTab) {
770
                components.useCases.searchUseCases.newTabSearch
771
772
                    .invoke(
                        searchTerms,
773
                        SessionState.Source.USER_ENTERED,
774
                        true,
775
                        mode.isPrivate,
776
777
                        searchEngine = engine
                    )
778
            } else components.useCases.searchUseCases.defaultSearch.invoke(searchTerms, engine)
779
780
        }

781
        if (!forceSearch && searchTermOrURL.isUrl()) {
Sebastian Kaspari's avatar
Sebastian Kaspari committed
782
            loadUrlUseCase.invoke(searchTermOrURL.toNormalizedUrl(), flags)
783
        } else {
784
            searchUseCase.invoke(searchTermOrURL)
785
        }
786
787
788
789

        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.
790
791
792
793
794
            components.core.engine.profiler?.addMarker(
                "HomeActivity.load",
                startTime,
                "newTab: $newTab"
            )
795
        }
796
797
    }

798
799
800
    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
801
802
803
804
        if (FeatureFlags.returnToBrowserOnColdStart &&
            settings().shouldReturnToBrowser &&
            !browsingModeManager.mode.isPrivate
        ) {
805
806
807
808
            openToBrowser(BrowserDirection.FromGlobal, null)
        }
    }

809
    override fun attachBaseContext(base: Context) {
810
        base.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
811
812
813
814
            super.attachBaseContext(base)
        }
    }

815
    protected open fun createBrowsingModeManager(initialMode: BrowsingMode): BrowsingModeManager {
816
        return DefaultBrowsingModeManager(initialMode, components.settings) { newMode ->
817
818
819
820
            themeManager.currentTheme = newMode
        }
    }

821
822
    protected open fun createThemeManager(): ThemeManager {
        return DefaultThemeManager(browsingModeManager.mode, this)
Jeff Boek's avatar
Jeff Boek committed
823
824
    }

Gabriel Luong's avatar
Gabriel Luong committed
825
826
827
828
829
830
831
832
    private fun openPopup(webExtensionState: WebExtensionState) {
        val action = NavGraphDirections.actionGlobalWebExtensionActionPopupFragment(
            webExtensionId = webExtensionState.id,
            webExtensionTitle = webExtensionState.name
        )
        navHost.navController.navigate(action)
    }

833
834
835
836
    /**
     * The root container is null at this point, so let the HomeActivity know that
     * we are visually complete.
     */
837
    fun setVisualCompletenessQueueReady() {
838
839
840
        isVisuallyComplete = true
    }

841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
    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)
        }
    }

858
    @VisibleForTesting
859
    internal fun isActivityColdStarted(startingIntent: Intent, activityIcicle: Bundle?): Boolean {
860
861
        // First time opening this activity in the task.
        // Cold start / start from Recents after back press.
862
863
864
865
866
        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
    }
867

868
869
    companion object {
        const val OPEN_TO_BROWSER = "open_to_browser"
Yeon Taek Jeong's avatar
Yeon Taek Jeong committed
870
871
        const val OPEN_TO_BROWSER_AND_LOAD = "open_to_browser_and_load"
        const val OPEN_TO_SEARCH = "open_to_search"
872
        const val PRIVATE_BROWSING_MODE = "private_browsing_mode"
873
874
        const val EXTRA_DELETE_PRIVATE_TABS = "notification_delete_and_open"
        const val EXTRA_OPENED_FROM_NOTIFICATION = "notification_open"
875
        const val START_IN_RECENTS_SCREEN = "start_in_recents_screen"
876
877
878
879

        // 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
880
    }
Jeff Boek's avatar
Jeff Boek committed
881
}