HomeActivity.kt 34.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.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.localbroadcastmanager.content.LocalBroadcastManager
29
import androidx.navigation.NavDestination
30
import androidx.navigation.NavDirections
31
import androidx.navigation.fragment.NavHostFragment
32
33
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
34
import kotlinx.android.synthetic.main.activity_home.*
Gabriel Luong's avatar
Gabriel Luong committed
35
import kotlinx.coroutines.CoroutineScope
36
import kotlinx.coroutines.Dispatchers
37
import kotlinx.coroutines.Dispatchers.IO
38
import kotlinx.coroutines.Dispatchers.Main
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.session.SessionManager
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.UriOpenedObserver
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.resetPoliciesAfter
80
import org.mozilla.fenix.ext.settings
81
import org.mozilla.fenix.home.HomeFragmentDirections
82
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
83
84
import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor
import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor
85
import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor
86
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
87
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
88
89
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.history.HistoryFragmentDirections
ekager's avatar
ekager committed
90
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
91
import org.mozilla.fenix.perf.Performance
92
import org.mozilla.fenix.perf.StartupTimeline
93
import org.mozilla.fenix.search.SearchFragmentDirections
94
import org.mozilla.fenix.searchdialog.SearchDialogFragmentDirections
95
import org.mozilla.fenix.session.PrivateNotificationService
96
import org.mozilla.fenix.settings.SettingsFragmentDirections
97
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
98
import org.mozilla.fenix.settings.about.AboutFragmentDirections
99
import org.mozilla.fenix.settings.logins.fragment.LoginDetailFragmentDirections
100
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections
101
102
103
import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
104
import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
105
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
106
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
107
108
import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager
109
import org.mozilla.fenix.utils.BrowsersCache
110
111
112
import org.torproject.android.service.TorService
import org.torproject.android.service.TorServiceConstants
import org.torproject.android.service.util.Prefs
113
import java.lang.ref.WeakReference
Jeff Boek's avatar
Jeff Boek committed
114

115
116
117
118
119
120
/**
 * 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
 */
121
@OptIn(ExperimentalCoroutinesApi::class)
122
@SuppressWarnings("TooManyFunctions", "LargeClass")
123
open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
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
    private lateinit var sessionObserver: SessionManager.Observer
129

130
131
    private var isVisuallyComplete = false

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

134
135
    private var isToolbarInflated = false

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

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

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

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

157
158
    private lateinit var navigationToolbar: Toolbar

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

163
164
        StrictModeManager.changeStrictModePolicies(supportFragmentManager)
        // There is disk read violations on some devices such as samsung and pixel for android 9/10
165
        StrictMode.allowThreadDiskReads().resetPoliciesAfter {
166
167
            super.onCreate(savedInstanceState)
        }
168

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

179
        components.publicSuffixList.prefetch()
180

181
        setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent))
182
        setContentView(R.layout.activity_home)
183
184
185

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

190
191
        sessionObserver = UriOpenedObserver(this)

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

201
        if (isActivityColdStarted(intent, savedInstanceState)) {
202
203
204
205
206
207
208
            externalSourceIntentProcessors.any {
                it.process(
                    intent,
                    navHost.navController,
                    this.intent
                )
            }
209
        }
210

211
        Performance.processIntentIfPerformanceTest(intent, this)
212

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

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

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

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

        captureSnapshotTelemetryMetrics()
243

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

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

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

    override fun onRestart() {
        super.onRestart()

        components.appStartupTelemetry.onHomeActivityOnRestart()
257
258
    }

259
    @CallSuper
260
261
    override fun onResume() {
        super.onResume()
262

263
264
265
266
267
268
        // Diagnostic breadcrumb for "Display already aquired" crash:
        // https://github.com/mozilla-mobile/android-components/issues/7960
        breadcrumb(
            message = "onResume()"
        )

269
270
        components.appStartupTelemetry.onHomeActivityOnResume()

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

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

            settings().wasDefaultBrowserOnLastResume = settings().isDefaultBrowser()
295
296

            if (!settings().manuallyCloseTabs) {
297
                val toClose = components.core.store.state.tabs.filter {
298
                    (System.currentTimeMillis() - it.lastAccess) > settings().getTabTimeout()
299
300
301
302
                }
                // Removal needs to happen on the main thread.
                lifecycleScope.launch(Main) {
                    toClose.forEach { components.useCases.tabsUseCases.removeTab(it.id) }
303
304
                }
            }
305
        }
306
307
    }

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

331
    final override fun onPause() {
332
333
334
        if (settings().lastKnownMode.isPrivate) {
            window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
        }
335

336
337
        super.onPause()

338
339
340
341
342
343
344
345
346
        // 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()
            )
        )

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

355
356
    override fun onDestroy() {
        super.onDestroy()
357
358
359
360
361
362
363
364
365
366

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

367
368
369
        privateNotificationObserver?.stop()
    }

370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
    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()
    }

390
391
392
393
394
    /**
     * Handles intents received when the activity is open.
     */
    final override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
395
396
        intent ?: return

397
398
399
400
401
402
403
404
405
        // 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()
            )
        )

406
407
408
409
        val intentProcessors =
            listOf(CrashReporterIntentProcessor()) + externalSourceIntentProcessors
        val intentHandled =
            intentProcessors.any { it.process(intent, navHost.navController, this.intent) }
410
        browsingModeManager.mode = getModeFromIntentOrLastKnown(intent)
411
412
413
414
415
416
417
418
419
420

        if (intentHandled) {
            supportFragmentManager
                .primaryNavigationFragment
                ?.childFragmentManager
                ?.fragments
                ?.lastOrNull()
                ?.let { it as? TabTrayDialogFragment }
                ?.also { it.dismissAllowingStateLoss() }
        }
421
422
423
424
425
426
427

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

430
        components.appStartupTelemetry.onHomeActivityOnNewIntent(intent.toSafeIntent())
431
432
433
434
435
436
    }

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

458
459
460
    @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
461
462
        val order = hashMapOf<String, Int>()

463
464
465
466
        order["CUSTOM_CONTEXT_MENU_EMAIL"] = 0
        order["CUSTOM_CONTEXT_MENU_CALL"] = 1
        order["org.mozilla.geckoview.COPY"] = 2
        order["CUSTOM_CONTEXT_MENU_SEARCH"] = 3
467
        order["CUSTOM_CONTEXT_MENU_SEARCH_PRIVATELY"] = 4
468
469
470
        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
471
472
473
474
475
476
477

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

478
    final override fun onBackPressed() {
479
        supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
480
            if (it is UserInteractionHandler && it.onBackPressed()) {
481
482
483
484
485
                return
            }
        }
        super.onBackPressed()
    }
486

487
488
489
490
491
492
493
494
    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
    }
495
496
497
498
499
500
501
502
503
504
505
506

    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
507
        // Android N and Huawei devices have broken onKeyLongPress events for the back button, so we
508
509
510
511
        // 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
512
        if (shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
513
514
515
516
517
518
519
520
521
            backLongPressJob = lifecycleScope.launch {
                delay(ViewConfiguration.getLongPressTimeout().toLong())
                handleBackLongPress()
            }
        }
        return super.onKeyDown(keyCode, event)
    }

    final override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
522
        if (shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
523
524
525
526
527
528
529
530
            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.
531
        if (!shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
532
533
534
535
536
            return handleBackLongPress()
        }
        return super.onKeyLongPress(keyCode, event)
    }

537
538
539
540
541
542
543
544
545
546
    final override fun onUserLeaveHint() {
        supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
            if (it is UserInteractionHandler && it.onHomePressed()) {
                return
            }
        }

        super.onUserLeaveHint()
    }

547
    protected open fun getBreadcrumbMessage(destination: NavDestination): String {
548
549
550
551
552
553
554
555
556
557
558
559
560
        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
        }
    }

561
    protected open fun getIntentAllSource(intent: SafeIntent): Event.AppReceivedIntent.Source? {
562
        return when {
563
564
565
            intent.isLauncherIntent -> Event.AppReceivedIntent.Source.APP_ICON
            intent.action == Intent.ACTION_VIEW -> Event.AppReceivedIntent.Source.LINK
            else -> Event.AppReceivedIntent.Source.UNKNOWN
566
567
568
        }
    }

569
570
    /**
     * External sources such as 3rd party links and shortcuts use this function to enter
571
572
     * private mode directly before the content view is created. Returns the mode set by the intent
     * otherwise falls back to the last known mode.
573
     */
574
    internal fun getModeFromIntentOrLastKnown(intent: Intent?): BrowsingMode {
575
576
        intent?.toSafeIntent()?.let {
            if (it.hasExtra(PRIVATE_BROWSING_MODE)) {
577
578
579
                val startPrivateMode = settings().shouldDisableNormalMode ||
                    it.getBooleanExtra(PRIVATE_BROWSING_MODE, settings().openLinksInAPrivateTab)

580
                return BrowsingMode.fromBoolean(isPrivate = startPrivateMode)
581
582
            }
        }
583
584
585
586
587
        return when {
            settings().shouldDisableNormalMode -> BrowsingMode.Private
            settings().openLinksInAPrivateTab -> BrowsingMode.Private
            else -> settings().lastKnownMode
        }
588
589
    }

590
591
592
593
594
595
596
597
598
599
600
601
602
603
    /**
     * 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) {
604
605
        val shouldStartPrivate = settings().shouldDisableNormalMode ||
                    intent.getStringExtra(OPEN_TO_SEARCH) ==
606
607
                    StartSearchIntentProcessor.STATIC_SHORTCUT_NEW_PRIVATE_TAB ||
                    intent.getStringExtra(OPEN_TO_SEARCH) ==
608
609
                    StartSearchIntentProcessor.PRIVATE_BROWSING_PINNED_SHORTCUT
        if (intent.hasExtra(OPEN_TO_SEARCH) && shouldStartPrivate) {
610
            PrivateNotificationService.isStartedFromPrivateShortcut = true
611
612
613
        }
    }

614
    private fun setupThemeAndBrowsingMode(mode: BrowsingMode) {
615
        settings().lastKnownMode = mode
616
        browsingModeManager = createBrowsingModeManager(mode)
617
618
619
620
621
        themeManager = createThemeManager()
        themeManager.setActivityTheme(this)
        themeManager.applyStatusBarTheme(this)
    }

622
623
624
625
    /**
     * Returns the [supportActionBar], inflating it if necessary.
     * Everyone should call this instead of supportActionBar.
     */
626
    override fun getSupportActionBarAndInflateIfNecessary(): ActionBar {
627
        if (!isToolbarInflated) {
628
            navigationToolbar = navigationToolbarStub.inflate() as Toolbar
629
630

            setSupportActionBar(navigationToolbar)
631
            // Add ids to this that we don't want to have a toolbar back button
632
            setupNavigationToolbar()
633

634
635
636
            isToolbarInflated = true
        }
        return supportActionBar!!
637
638
    }

639
640
641
642
643
644
645
646
647
648
649
650
651
    @Suppress("SpreadOperator")
    fun setupNavigationToolbar(vararg topLevelDestinationIds: Int) {
        NavigationUI.setupWithNavController(
            navigationToolbar,
            navHost.navController,
            AppBarConfiguration.Builder(*topLevelDestinationIds).build()
        )

        navigationToolbar.setNavigationOnClickListener {
            onBackPressed()
        }
    }

652
653
    protected open fun getIntentSessionId(intent: SafeIntent): String? = null

Sebastian Kaspari's avatar
Sebastian Kaspari committed
654
655
656
657
658
659
    /**
     * 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).
     */
660
    @Suppress("LongParameterList")
661
    fun openToBrowserAndLoad(
662
        searchTermOrURL: String,
663
664
        newTab: Boolean,
        from: BrowserDirection,
665
        customTabSessionId: String? = null,
666
        engine: SearchEngine? = null,
Sebastian Kaspari's avatar
Sebastian Kaspari committed
667
668
        forceSearch: Boolean = false,
        flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none()
669
    ) {
670
        openToBrowser(from, customTabSessionId)
Sebastian Kaspari's avatar
Sebastian Kaspari committed
671
        load(searchTermOrURL, newTab, engine, forceSearch, flags)
672
673
    }

674
    fun openToBrowser(from: BrowserDirection, customTabSessionId: String? = null) {
675
676
        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
677
        val directions = getNavDirections(from, customTabSessionId)
678
679
680
        if (directions != null) {
            navHost.navController.nav(fragmentId, directions)
        }
681
682
    }

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

Sebastian Kaspari's avatar
Sebastian Kaspari committed
729
730
731
732
733
    /**
     * 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).
     */
734
735
736
737
    private fun load(
        searchTermOrURL: String,
        newTab: Boolean,
        engine: SearchEngine?,
Sebastian Kaspari's avatar
Sebastian Kaspari committed
738
739
        forceSearch: Boolean,
        flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none()
740
    ) {
741
        val startTime = components.core.engine.profiler?.getProfilerTime()
742
        val mode = browsingModeManager.mode
743

744
        val loadUrlUseCase = if (newTab) {
745
746
747
            when (mode) {
                BrowsingMode.Private -> components.useCases.tabsUseCases.addPrivateTab
                BrowsingMode.Normal -> components.useCases.tabsUseCases.addTab
748
749
750
751
            }
        } else components.useCases.sessionUseCases.loadUrl

        val searchUseCase: (String) -> Unit = { searchTerms ->
752
            if (newTab) {
753
                components.useCases.searchUseCases.newTabSearch
754
755
                    .invoke(
                        searchTerms,
756
                        SessionState.Source.USER_ENTERED,
757
                        true,
758
                        mode.isPrivate,
759
760
                        searchEngine = engine
                    )
761
            } else components.useCases.searchUseCases.defaultSearch.invoke(searchTerms, engine)
762
763
        }

764
        if (!forceSearch && searchTermOrURL.isUrl()) {
Sebastian Kaspari's avatar
Sebastian Kaspari committed
765
            loadUrlUseCase.invoke(searchTermOrURL.toNormalizedUrl(), flags)
766
        } else {
767
            searchUseCase.invoke(searchTermOrURL)
768
        }
769
770
771
772
773
774

        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.
            components.core.engine.profiler?.addMarker("HomeActivity.load", startTime, "newTab: $newTab")
        }
775
776
    }

777
    override fun attachBaseContext(base: Context) {
778
        StrictMode.allowThreadDiskReads().resetPoliciesAfter {
779
780
781
782
            super.attachBaseContext(base)
        }
    }

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

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

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

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

809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
    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)
        }
    }

826
    @VisibleForTesting
827
    internal fun isActivityColdStarted(startingIntent: Intent, activityIcicle: Bundle?): Boolean {
828
829
        // First time opening this activity in the task.
        // Cold start / start from Recents after back press.
830
831
832
833
834
        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
    }
835

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

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