HomeActivity.kt 37.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

Alex Catarineu's avatar
Alex Catarineu committed
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
Alex Catarineu's avatar
Alex Catarineu committed
47
48
import mozilla.components.feature.app.links.RedirectDialogFragment
import mozilla.components.feature.app.links.SimpleRedirectDialogFragment
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
Alex Catarineu's avatar
Alex Catarineu committed
62
import mozilla.components.support.utils.TorUtils
63
import mozilla.components.support.utils.toSafeIntent
Gabriel Luong's avatar
Gabriel Luong committed
64
import mozilla.components.support.webextensions.WebExtensionPopupFeature
65
import org.mozilla.fenix.GleanMetrics.Metrics
66
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections
67
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections
68
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
69
70
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
71
import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
72
import org.mozilla.fenix.components.metrics.Event
73
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
74
import org.mozilla.fenix.ext.alreadyOnDestination
75
import org.mozilla.fenix.ext.breadcrumb
76
import org.mozilla.fenix.ext.components
77
import org.mozilla.fenix.ext.metrics
78
import org.mozilla.fenix.ext.nav
79
import org.mozilla.fenix.ext.settings
80
import org.mozilla.fenix.home.HomeFragmentDirections
81
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
82
83
import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor
import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor
84
import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor
85
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
86
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
87
88
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.history.HistoryFragmentDirections
ekager's avatar
ekager committed
89
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
90
import org.mozilla.fenix.perf.Performance
91
import org.mozilla.fenix.perf.StartupTimeline
92
import org.mozilla.fenix.search.SearchDialogFragmentDirections
93
import org.mozilla.fenix.session.PrivateNotificationService
94
import org.mozilla.fenix.settings.SettingsFragmentDirections
95
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
96
import org.mozilla.fenix.settings.about.AboutFragmentDirections
97
import org.mozilla.fenix.settings.logins.fragment.LoginDetailFragmentDirections
98
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections
99
100
101
import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
102
import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
103
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
104
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
105
106
import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager
107
import org.mozilla.fenix.utils.BrowsersCache
108
import java.lang.ref.WeakReference
Jeff Boek's avatar
Jeff Boek committed
109

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

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

129
130
    private var isVisuallyComplete = false

131
132
    private var privateNotificationObserver: PrivateNotificationFeature<PrivateNotificationService>? =
        null
133

134
135
    private var isToolbarInflated = false

Matthew Finkel's avatar
Matthew Finkel committed
136
137
    private var isBeingRecreated = 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

Alex Catarineu's avatar
Alex Catarineu committed
161
162
    private var dialog: RedirectDialogFragment? = null

163
    final override fun onCreate(savedInstanceState: Bundle?) {
164
        components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager)
Matthew Finkel's avatar
Matthew Finkel committed
165

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

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

181
        components.publicSuffixList.prefetch()
182

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

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

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

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

214
        Performance.processIntentIfPerformanceTest(intent, this)
215

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

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

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

        if (shouldAddToRecentsScreen(intent)) {
            intent.removeExtra(START_IN_RECENTS_SCREEN)
            moveTaskToBack(true)
        }
244
245

        captureSnapshotTelemetryMetrics()
246

247
        startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null)
248

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

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

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

270
271
    private fun startupTelemetryOnRestartCalled() {
        components.appStartupTelemetry.onHomeActivityOnRestart(rootContainer)
272
273
    }

274
    @CallSuper
275
276
    override fun onResume() {
        super.onResume()
277

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

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

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

            settings().wasDefaultBrowserOnLastResume = settings().isDefaultBrowser()
308
        }
309
310
    }

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

        components.appStartupTelemetry.onStop()
334
335
    }

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

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

345
346
347
348
        // 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

349
350
        super.onPause()

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

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

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

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

380
        privateNotificationObserver?.stop()
Matthew Finkel's avatar
Matthew Finkel committed
381
382
383
384
385
386
387
388
        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()
        }
389
390
    }

391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
    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()"
        )

Matthew Finkel's avatar
Matthew Finkel committed
408
409
        isBeingRecreated = true

410
411
412
        super.recreate()
    }

Alex Catarineu's avatar
Alex Catarineu committed
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
    // Copied from mozac AppLinksFeature.kt
    internal fun getOrCreateDialog(): RedirectDialogFragment {
        val existingDialog = dialog
        if (existingDialog != null) {
            return existingDialog
        }

        SimpleRedirectDialogFragment.newInstance().also {
            dialog = it
            return it
        }
    }
    private fun isAlreadyADialogCreated(): Boolean {
        return findPreviousDialogFragment() != null
    }

    private fun findPreviousDialogFragment(): RedirectDialogFragment? {
        return supportFragmentManager.findFragmentByTag(RedirectDialogFragment.FRAGMENT_TAG) as? RedirectDialogFragment
    }

433
434
435
436
437
    /**
     * Handles intents received when the activity is open.
     */
    final override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
438
439
440
441
        intent?.let {
            handleNewIntent(it)
        }
    }
442

443
    open fun handleNewIntent(intent: Intent) {
Alex Catarineu's avatar
Alex Catarineu committed
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
        val startIntent = intent.getParcelableExtra<PendingIntent>(TorUtils.TORBROWSER_START_ACTIVITY_PROMPT)
        if (startIntent != null) {
            if (startIntent.creatorPackage == applicationContext.packageName) {
                val dialog = getOrCreateDialog()
                dialog.onConfirmRedirect = {
                    @Suppress("EmptyCatchBlock")
                    try {
                        startIntent.send()
                    } catch (error: PendingIntent.CanceledException) {
                    }
                }
                dialog.onCancelRedirect = {}

                if (!isAlreadyADialogCreated()) {
                    dialog.showNow(supportFragmentManager, RedirectDialogFragment.FRAGMENT_TAG)
                }
            }
            return
        }

464
465
466
467
468
469
470
471
472
        // 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()
            )
        )

473
474
475
476
        val intentProcessors =
            listOf(CrashReporterIntentProcessor()) + externalSourceIntentProcessors
        val intentHandled =
            intentProcessors.any { it.process(intent, navHost.navController, this.intent) }
477
        browsingModeManager.mode = getModeFromIntentOrLastKnown(intent)
478
479
480
481
482
483
484
485
486
487

        if (intentHandled) {
            supportFragmentManager
                .primaryNavigationFragment
                ?.childFragmentManager
                ?.fragments
                ?.lastOrNull()
                ?.let { it as? TabTrayDialogFragment }
                ?.also { it.dismissAllowingStateLoss() }
        }
488
489
490
491
492
493
494

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

497
        components.appStartupTelemetry.onHomeActivityOnNewIntent(intent.toSafeIntent())
498
499
500
501
502
503
    }

    /**
     * Overrides view inflation to inject a custom [EngineView] from [components].
     */
    final override fun onCreateView(
504
505
506
507
        parent: View?,
        name: String,
        context: Context,
        attrs: AttributeSet
508
    ): View? = when (name) {
509
510
        EngineView::class.java.name -> components.core.engine.createView(context, attrs).apply {
            selectionActionDelegate = DefaultSelectionActionDelegate(
511
512
513
514
                BrowserStoreSearchAdapter(
                    components.core.store,
                    tabId = getIntentSessionId(intent.toSafeIntent())
                ),
Sawyer Blatz's avatar
Sawyer Blatz committed
515
516
517
518
519
520
                resources = context.resources,
                shareTextClicked = { share(it) },
                emailTextClicked = { email(it) },
                callTextClicked = { call(it) },
                actionSorter = ::actionSorter
            )
521
        }.asView()
522
523
        else -> super.onCreateView(parent, name, context, attrs)
    }
524

525
526
527
    @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
528
529
        val order = hashMapOf<String, Int>()

530
531
532
533
        order["CUSTOM_CONTEXT_MENU_EMAIL"] = 0
        order["CUSTOM_CONTEXT_MENU_CALL"] = 1
        order["org.mozilla.geckoview.COPY"] = 2
        order["CUSTOM_CONTEXT_MENU_SEARCH"] = 3
534
        order["CUSTOM_CONTEXT_MENU_SEARCH_PRIVATELY"] = 4
535
536
537
        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
538
539
540
541
542
543
544

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

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

554
555
556
557
558
559
560
561
    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
    }
562
563
564
565
566
567
568
569
570
571
572
573

    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
574
        // Android N and Huawei devices have broken onKeyLongPress events for the back button, so we
575
576
577
578
        // 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
579
        if (shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
580
581
582
583
584
585
586
587
588
            backLongPressJob = lifecycleScope.launch {
                delay(ViewConfiguration.getLongPressTimeout().toLong())
                handleBackLongPress()
            }
        }
        return super.onKeyDown(keyCode, event)
    }

    final override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
589
        if (shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
590
591
592
593
594
595
596
597
            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.
598
        if (!shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
599
600
601
602
603
            return handleBackLongPress()
        }
        return super.onKeyLongPress(keyCode, event)
    }

604
605
606
607
608
609
610
611
612
613
    final override fun onUserLeaveHint() {
        supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
            if (it is UserInteractionHandler && it.onHomePressed()) {
                return
            }
        }

        super.onUserLeaveHint()
    }

614
    protected open fun getBreadcrumbMessage(destination: NavDestination): String {
615
616
617
618
619
620
621
622
623
624
625
626
627
        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
        }
    }

628
    protected open fun getIntentAllSource(intent: SafeIntent): Event.AppReceivedIntent.Source? {
629
        return when {
630
631
632
            intent.isLauncherIntent -> Event.AppReceivedIntent.Source.APP_ICON
            intent.action == Intent.ACTION_VIEW -> Event.AppReceivedIntent.Source.LINK
            else -> Event.AppReceivedIntent.Source.UNKNOWN
633
634
635
        }
    }

636
637
    /**
     * External sources such as 3rd party links and shortcuts use this function to enter
638
639
     * private mode directly before the content view is created. Returns the mode set by the intent
     * otherwise falls back to the last known mode.
640
     */
641
    internal fun getModeFromIntentOrLastKnown(intent: Intent?): BrowsingMode {
642
643
        intent?.toSafeIntent()?.let {
            if (it.hasExtra(PRIVATE_BROWSING_MODE)) {
Alex Catarineu's avatar
Alex Catarineu committed
644
645
646
                val startPrivateMode = settings().shouldDisableNormalMode ||
                    it.getBooleanExtra(PRIVATE_BROWSING_MODE, settings().openLinksInAPrivateTab)

647
                return BrowsingMode.fromBoolean(isPrivate = startPrivateMode)
648
649
            }
        }
Alex Catarineu's avatar
Alex Catarineu committed
650
651
652
653
654
        return when {
            settings().shouldDisableNormalMode -> BrowsingMode.Private
            settings().openLinksInAPrivateTab -> BrowsingMode.Private
            else -> settings().lastKnownMode
        }
655
656
    }

657
658
659
660
661
662
663
664
665
666
667
668
669
    /**
     * 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
    }

670
    private fun setupThemeAndBrowsingMode(mode: BrowsingMode) {
671
        settings().lastKnownMode = mode
672
        browsingModeManager = createBrowsingModeManager(mode)
673
674
675
676
677
        themeManager = createThemeManager()
        themeManager.setActivityTheme(this)
        themeManager.applyStatusBarTheme(this)
    }

678
679
680
681
    /**
     * Returns the [supportActionBar], inflating it if necessary.
     * Everyone should call this instead of supportActionBar.
     */
682
    override fun getSupportActionBarAndInflateIfNecessary(): ActionBar {
683
        if (!isToolbarInflated) {
684
            navigationToolbar = navigationToolbarStub.inflate() as Toolbar
685
686

            setSupportActionBar(navigationToolbar)
687
            // Add ids to this that we don't want to have a toolbar back button
688
            setupNavigationToolbar()
689

690
691
692
            isToolbarInflated = true
        }
        return supportActionBar!!
693
694
    }

695
696
697
698
699
700
701
702
703
704
705
706
707
    @Suppress("SpreadOperator")
    fun setupNavigationToolbar(vararg topLevelDestinationIds: Int) {
        NavigationUI.setupWithNavController(
            navigationToolbar,
            navHost.navController,
            AppBarConfiguration.Builder(*topLevelDestinationIds).build()
        )

        navigationToolbar.setNavigationOnClickListener {
            onBackPressed()
        }
    }

708
709
    protected open fun getIntentSessionId(intent: SafeIntent): String? = null

Sebastian Kaspari's avatar
Sebastian Kaspari committed
710
711
712
713
714
715
    /**
     * 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).
     */
716
    @Suppress("LongParameterList")
717
    fun openToBrowserAndLoad(
718
        searchTermOrURL: String,
719
720
        newTab: Boolean,
        from: BrowserDirection,
721
        customTabSessionId: String? = null,
722
        engine: SearchEngine? = null,
Sebastian Kaspari's avatar
Sebastian Kaspari committed
723
724
        forceSearch: Boolean = false,
        flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none()
725
    ) {
726
        openToBrowser(from, customTabSessionId)
Sebastian Kaspari's avatar
Sebastian Kaspari committed
727
        load(searchTermOrURL, newTab, engine, forceSearch, flags)
728
729
    }

730
    fun openToBrowser(from: BrowserDirection, customTabSessionId: String? = null) {
731
732
        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
733
        val directions = getNavDirections(from, customTabSessionId)
734
735
736
        if (directions != null) {
            navHost.navController.nav(fragmentId, directions)
        }
737
738
    }

Tiger Oakes's avatar
Tiger Oakes committed
739
740
741
    protected open fun getNavDirections(
        from: BrowserDirection,
        customTabSessionId: String?
742
    ): NavDirections? = when (from) {
Tiger Oakes's avatar
Tiger Oakes committed
743
744
745
        BrowserDirection.FromGlobal ->
            NavGraphDirections.actionGlobalBrowser(customTabSessionId)
        BrowserDirection.FromHome ->
746
            HomeFragmentDirections.actionGlobalBrowser(customTabSessionId)
747
748
        BrowserDirection.FromSearchDialog ->
            SearchDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
Tiger Oakes's avatar
Tiger Oakes committed
749
        BrowserDirection.FromSettings ->
Jeff Boek's avatar
Jeff Boek committed
750
            SettingsFragmentDirections.actionGlobalBrowser(customTabSessionId)
751
752
        BrowserDirection.FromSyncedTabs ->
            SyncedTabsFragmentDirections.actionGlobalBrowser(customTabSessionId)
Tiger Oakes's avatar
Tiger Oakes committed
753
        BrowserDirection.FromBookmarks ->
Jeff Boek's avatar
Jeff Boek committed
754
            BookmarkFragmentDirections.actionGlobalBrowser(customTabSessionId)
Tiger Oakes's avatar
Tiger Oakes committed
755
        BrowserDirection.FromHistory ->
Jeff Boek's avatar
Jeff Boek committed
756
            HistoryFragmentDirections.actionGlobalBrowser(customTabSessionId)
757
758
        BrowserDirection.FromTrackingProtectionExceptions ->
            TrackingProtectionExceptionsFragmentDirections.actionGlobalBrowser(customTabSessionId)
759
        BrowserDirection.FromAbout ->
Jeff Boek's avatar
Jeff Boek committed
760
            AboutFragmentDirections.actionGlobalBrowser(customTabSessionId)
761
        BrowserDirection.FromTrackingProtection ->
Jeff Boek's avatar
Jeff Boek committed
762
            TrackingProtectionFragmentDirections.actionGlobalBrowser(customTabSessionId)
763
        BrowserDirection.FromSavedLoginsFragment ->
764
            SavedLoginsAuthFragmentDirections.actionGlobalBrowser(customTabSessionId)
765
766
767
768
769
770
        BrowserDirection.FromAddNewDeviceFragment ->
            AddNewDeviceFragmentDirections.actionGlobalBrowser(customTabSessionId)
        BrowserDirection.FromAddSearchEngineFragment ->
            AddSearchEngineFragmentDirections.actionGlobalBrowser(customTabSessionId)
        BrowserDirection.FromEditCustomSearchEngineFragment ->
            EditCustomSearchEngineFragmentDirections.actionGlobalBrowser(customTabSessionId)
771
        BrowserDirection.FromAddonDetailsFragment ->
772
            AddonDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
773
774
        BrowserDirection.FromAddonPermissionsDetailsFragment ->
            AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
775
776
        BrowserDirection.FromLoginDetailFragment ->
            LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
777
778
        BrowserDirection.FromTabTray ->
            TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
ekager's avatar
ekager committed
779
780
        BrowserDirection.FromRecentlyClosed ->
            RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId)
Tiger Oakes's avatar
Tiger Oakes committed
781
782
    }

Sebastian Kaspari's avatar
Sebastian Kaspari committed
783
784
785
786
787
    /**
     * 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).
     */
788
789
790
791
    private fun load(
        searchTermOrURL: String,
        newTab: Boolean,
        engine: SearchEngine?,
Sebastian Kaspari's avatar
Sebastian Kaspari committed
792
793
        forceSearch: Boolean,
        flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none()
794
    ) {
795
        val startTime = components.core.engine.profiler?.getProfilerTime()
796
        val mode = browsingModeManager.mode
797

798
        val loadUrlUseCase = if (newTab) {
799
800
801
            when (mode) {
                BrowsingMode.Private -> components.useCases.tabsUseCases.addPrivateTab
                BrowsingMode.Normal -> components.useCases.tabsUseCases.addTab
802
803
804
805
            }
        } else components.useCases.sessionUseCases.loadUrl

        val searchUseCase: (String) -> Unit = { searchTerms ->
806
            if (newTab) {
807
                components.useCases.searchUseCases.newTabSearch
808
809
                    .invoke(
                        searchTerms,
810
                        SessionState.Source.USER_ENTERED,
811
                        true,
812
                        mode.isPrivate,
813
814
                        searchEngine = engine
                    )
815
            } else components.useCases.searchUseCases.defaultSearch.invoke(searchTerms, engine)
816
817
        }

818
        if (!forceSearch && searchTermOrURL.isUrl()) {
Sebastian Kaspari's avatar
Sebastian Kaspari committed
819
            loadUrlUseCase.invoke(searchTermOrURL.toNormalizedUrl(), flags)
820
        } else {
821
            searchUseCase.invoke(searchTermOrURL)
822
        }
823
824
825
826

        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.
827
828
829
830
831
            components.core.engine.profiler?.addMarker(
                "HomeActivity.load",
                startTime,
                "newTab: $newTab"
            )
832
        }
833
834
    }

835
836
837
    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
838
        if (settings().shouldReturnToBrowser &&
839
840
            !browsingModeManager.mode.isPrivate
        ) {
841
842
843
844
            openToBrowser(BrowserDirection.FromGlobal, null)
        }
    }

845
    override fun attachBaseContext(base: Context) {
846
        base.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
847
848
849
850
            super.attachBaseContext(base)
        }
    }

851
    protected open fun createBrowsingModeManager(initialMode: BrowsingMode): BrowsingModeManager {
852
        return DefaultBrowsingModeManager(initialMode, components.settings) { newMode ->
853
854
855
856
            themeManager.currentTheme = newMode
        }
    }

857
858
    protected open fun createThemeManager(): ThemeManager {
        return DefaultThemeManager(browsingModeManager.mode, this)
Jeff Boek's avatar
Jeff Boek committed
859
860
    }

Gabriel Luong's avatar
Gabriel Luong committed
861
862
863
864
865
866
867
868
    private fun openPopup(webExtensionState: WebExtensionState) {
        val action = NavGraphDirections.actionGlobalWebExtensionActionPopupFragment(
            webExtensionId = webExtensionState.id,
            webExtensionTitle = webExtensionState.name
        )
        navHost.navController.navigate(action)
    }

869
870
871
872
    /**
     * The root container is null at this point, so let the HomeActivity know that
     * we are visually complete.
     */
873
    fun setVisualCompletenessQueueReady() {
874
875
876
        isVisuallyComplete = true
    }

877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
    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)
        }
    }

894
    @VisibleForTesting
895
    internal fun isActivityColdStarted(startingIntent: Intent, activityIcicle: Bundle?): Boolean {
896
897
        // First time opening this activity in the task.
        // Cold start / start from Recents after back press.
898
899
900
901
902
        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
    }
903

904
905
    companion object {
        const val OPEN_TO_BROWSER = "open_to_browser"
Yeon Taek Jeong's avatar
Yeon Taek Jeong committed
906
907
        const val OPEN_TO_BROWSER_AND_LOAD = "open_to_browser_and_load"
        const val OPEN_TO_SEARCH = "open_to_search"
908
        const val PRIVATE_BROWSING_MODE = "private_browsing_mode"
909
910
        const val EXTRA_DELETE_PRIVATE_TABS = "notification_delete_and_open"
        const val EXTRA_OPENED_FROM_NOTIFICATION = "notification_open"
911
        const val START_IN_RECENTS_SCREEN = "start_in_recents_screen"
912
913
914
915

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