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

112
113
114
115
116
117
/**
 * 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
 */
118
@OptIn(ExperimentalCoroutinesApi::class)
119
@SuppressWarnings("TooManyFunctions", "LargeClass")
120
open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
121
122
123
124
125
    // 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()
126

Gabriel Luong's avatar
Gabriel Luong committed
127
    private var webExtScope: CoroutineScope? = null
Jeff Boek's avatar
Jeff Boek committed
128
    lateinit var themeManager: ThemeManager
129
    lateinit var browsingModeManager: BrowsingModeManager
130

131
132
    private var isVisuallyComplete = false

133
134
    private var privateNotificationObserver: PrivateNotificationFeature<PrivateNotificationService>? =
        null
135

136
137
    private var isToolbarInflated = false

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

142
143
144
145
    private val navHost by lazy {
        supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment
    }

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

156
157
158
    // See onKeyDown for why this is necessary
    private var backLongPressJob: Job? = null

159
160
    private lateinit var navigationToolbar: Toolbar

161
    final override fun onCreate(savedInstanceState: Bundle?) {
162
163
164
        // Give Orbot the base Context
        Prefs.setContext(applicationContext)

165
        components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager)
166

167
        // There is disk read violations on some devices such as samsung and pixel for android 9/10
168
        components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
169
170
            super.onCreate(savedInstanceState)
        }
171

172
173
174
175
176
177
178
179
180
181
        // 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")
            )
        )

182
        components.publicSuffixList.prefetch()
183

184
        setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent))
185
        setContentView(R.layout.activity_home)
186
187
188

        // Must be after we set the content view
        if (isVisuallyComplete) {
189
190
            components.performance.visualCompletenessQueue
                .attachViewToRunVisualCompletenessQueueLater(WeakReference(rootContainer))
191
192
        }

193
        checkPrivateShortcutEntryPoint(intent)
194
195
196
197
198
        privateNotificationObserver = PrivateNotificationFeature(
            applicationContext,
            components.core.store,
            PrivateNotificationService::class
        ).also {
199
200
            it.start()
        }
201

202
203
204
205
        if (isActivityColdStarted(
                intent,
                savedInstanceState
            ) && !externalSourceIntentProcessors.any {
206
207
208
209
210
211
                it.process(
                    intent,
                    navHost.navController,
                    this.intent
                )
            }
212
213
        ) {
            navigateToBrowserOnColdStart()
214
        }
215

216
        Performance.processIntentIfPerformanceTest(intent, this)
217

218
        if (settings().isTelemetryEnabled) {
219
220
221
222
223
224
            lifecycle.addObserver(
                BreadcrumbsRecorder(
                    components.analytics.crashReporter,
                    navHost.navController, ::getBreadcrumbMessage
                )
            )
225

226
227
            val safeIntent = intent?.toSafeIntent()
            safeIntent
228
229
                ?.let(::getIntentSource)
                ?.also { components.analytics.metrics.track(Event.OpenedApp(it)) }
230
231
232
            // record on cold startup
            safeIntent
                ?.let(::getIntentAllSource)
233
                ?.also { components.analytics.metrics.track(Event.AppReceivedIntent(it)) }
234
        }
235
        supportActionBar?.hide()
Gabriel Luong's avatar
Gabriel Luong committed
236

237
238
239
240
        lifecycle.addObservers(
            webExtensionPopupFeature,
            StartupTimeline.homeActivityLifecycleObserver
        )
241
242
243
244
245

        if (shouldAddToRecentsScreen(intent)) {
            intent.removeExtra(START_IN_RECENTS_SCREEN)
            moveTaskToBack(true)
        }
246
247

        captureSnapshotTelemetryMetrics()
248

249
        startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null)
250

251
        StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
252
    }
Jeff Boek's avatar
Jeff Boek committed
253

254
255
256
257
    protected open fun startupTelemetryOnCreateCalled(
        safeIntent: SafeIntent,
        hasSavedInstanceState: Boolean
    ) {
258
259
260
261
262
        components.appStartupTelemetry.onHomeActivityOnCreate(
            safeIntent,
            hasSavedInstanceState,
            homeActivityInitTimeStampNanoSeconds, rootContainer
        )
263
264
265
    }

    override fun onRestart() {
266
267
268
        // DO NOT MOVE ANYTHING ABOVE THIS..
        // we are measuring startup time for hot startup type
        startupTelemetryOnRestartCalled()
269
        super.onRestart()
270
    }
271

272
273
    private fun startupTelemetryOnRestartCalled() {
        components.appStartupTelemetry.onHomeActivityOnRestart(rootContainer)
274
275
    }

276
    @CallSuper
277
278
    override fun onResume() {
        super.onResume()
279

280
281
282
283
284
285
        // Diagnostic breadcrumb for "Display already aquired" crash:
        // https://github.com/mozilla-mobile/android-components/issues/7960
        breadcrumb(
            message = "onResume()"
        )

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

        // Launch this on a background thread so as not to affect startup performance
        lifecycleScope.launch(IO) {
Sawyer Blatz's avatar
Sawyer Blatz committed
302
303
            if (
                settings().isDefaultBrowser() &&
304
                settings().wasDefaultBrowserOnLastResume != settings().isDefaultBrowser()
Sawyer Blatz's avatar
Sawyer Blatz committed
305
            ) {
306
307
                metrics.track(Event.ChangedToDefaultBrowser)
            }
308
309

            settings().wasDefaultBrowserOnLastResume = settings().isDefaultBrowser()
310
        }
311
312
    }

313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
    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()
            )
        )
334
335

        components.appStartupTelemetry.onStop()
336
337
    }

338
    final override fun onPause() {
339
340
341
342
        // 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()

343
344
345
        if (settings().lastKnownMode.isPrivate) {
            window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
        }
346

347
348
349
350
        // 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

351
352
        super.onPause()

353
354
355
356
357
358
359
360
361
        // 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()
            )
        )

362
363
364
365
366
367
368
        // 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()
    }
369

370
371
    override fun onDestroy() {
        super.onDestroy()
372
373
374
375
376
377
378
379
380
381

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

382
383
384
        privateNotificationObserver?.stop()
    }

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

405
406
407
408
409
    /**
     * Handles intents received when the activity is open.
     */
    final override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
410
411
        intent ?: return

412
413
414
415
416
417
418
419
420
        // 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()
            )
        )

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

        if (intentHandled) {
            supportFragmentManager
                .primaryNavigationFragment
                ?.childFragmentManager
                ?.fragments
                ?.lastOrNull()
                ?.let { it as? TabTrayDialogFragment }
                ?.also { it.dismissAllowingStateLoss() }
        }
436
437
438
439
440
441
442

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

445
        components.appStartupTelemetry.onHomeActivityOnNewIntent(intent.toSafeIntent())
446
447
448
449
450
451
    }

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

473
474
475
    @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
476
477
        val order = hashMapOf<String, Int>()

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

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

493
    final override fun onBackPressed() {
494
        supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
495
            if (it is UserInteractionHandler && it.onBackPressed()) {
496
497
498
499
500
                return
            }
        }
        super.onBackPressed()
    }
501

502
503
504
505
506
507
508
509
    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
    }
510
511
512
513
514
515
516
517
518
519
520
521

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

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

552
553
554
555
556
557
558
559
560
561
    final override fun onUserLeaveHint() {
        supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
            if (it is UserInteractionHandler && it.onHomePressed()) {
                return
            }
        }

        super.onUserLeaveHint()
    }

562
    protected open fun getBreadcrumbMessage(destination: NavDestination): String {
563
564
565
566
567
568
569
570
571
572
573
574
575
        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
        }
    }

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

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

595
                return BrowsingMode.fromBoolean(isPrivate = startPrivateMode)
596
597
            }
        }
598
599
600
601
602
        return when {
            settings().shouldDisableNormalMode -> BrowsingMode.Private
            settings().openLinksInAPrivateTab -> BrowsingMode.Private
            else -> settings().lastKnownMode
        }
603
604
    }

605
606
607
608
609
610
611
612
613
614
615
616
617
618
    /**
     * 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) {
619
620
        val shouldStartPrivate = settings().shouldDisableNormalMode ||
                    intent.getStringExtra(OPEN_TO_SEARCH) ==
621
622
                    StartSearchIntentProcessor.STATIC_SHORTCUT_NEW_PRIVATE_TAB ||
                    intent.getStringExtra(OPEN_TO_SEARCH) ==
623
624
                    StartSearchIntentProcessor.PRIVATE_BROWSING_PINNED_SHORTCUT
        if (intent.hasExtra(OPEN_TO_SEARCH) && shouldStartPrivate) {
625
            PrivateNotificationService.isStartedFromPrivateShortcut = true
626
627
628
        }
    }

629
    private fun setupThemeAndBrowsingMode(mode: BrowsingMode) {
630
        settings().lastKnownMode = mode
631
        browsingModeManager = createBrowsingModeManager(mode)
632
633
634
635
636
        themeManager = createThemeManager()
        themeManager.setActivityTheme(this)
        themeManager.applyStatusBarTheme(this)
    }

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

            setSupportActionBar(navigationToolbar)
646
            // Add ids to this that we don't want to have a toolbar back button
647
            setupNavigationToolbar()
648

649
650
651
            isToolbarInflated = true
        }
        return supportActionBar!!
652
653
    }

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

        navigationToolbar.setNavigationOnClickListener {
            onBackPressed()
        }
    }

667
668
    protected open fun getIntentSessionId(intent: SafeIntent): String? = null

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

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

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

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

757
        val loadUrlUseCase = if (newTab) {
758
759
760
            when (mode) {
                BrowsingMode.Private -> components.useCases.tabsUseCases.addPrivateTab
                BrowsingMode.Normal -> components.useCases.tabsUseCases.addTab
761
762
763
764
            }
        } else components.useCases.sessionUseCases.loadUrl

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

777
        if (!forceSearch && searchTermOrURL.isUrl()) {
Sebastian Kaspari's avatar
Sebastian Kaspari committed
778
            loadUrlUseCase.invoke(searchTermOrURL.toNormalizedUrl(), flags)
779
        } else {
780
            searchUseCase.invoke(searchTermOrURL)
781
        }
782
783
784
785

        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.
786
787
788
789
790
            components.core.engine.profiler?.addMarker(
                "HomeActivity.load",
                startTime,
                "newTab: $newTab"
            )
791
        }
792
793
    }

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

805
    override fun attachBaseContext(base: Context) {
806
        base.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
807
808
809
810
            super.attachBaseContext(base)
        }
    }

811
    protected open fun createBrowsingModeManager(initialMode: BrowsingMode): BrowsingModeManager {
812
        return DefaultBrowsingModeManager(initialMode, components.settings) { newMode ->
813
814
815
816
            themeManager.currentTheme = newMode
        }
    }

817
818
    protected open fun createThemeManager(): ThemeManager {
        return DefaultThemeManager(browsingModeManager.mode, this)
Jeff Boek's avatar
Jeff Boek committed
819
820
    }

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

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

837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
    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)
        }
    }

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

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

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