1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.fenix.perf
7 import android.app.Activity
8 import android.content.Intent
9 import androidx.annotation.VisibleForTesting
10 import androidx.annotation.VisibleForTesting.NONE
11 import androidx.annotation.VisibleForTesting.PRIVATE
12 import androidx.lifecycle.DefaultLifecycleObserver
13 import androidx.lifecycle.Lifecycle
14 import androidx.lifecycle.LifecycleOwner
17 * The "path" that this activity started in. See the
18 * [Fenix perf glossary](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary)
19 * for specific definitions.
21 * This should be a member variable of [Activity] because its data is tied to the lifecycle of an
22 * Activity. Call [attachOnActivityOnCreate] & [onIntentReceived] for this class to work correctly.
24 class StartupPathProvider {
27 * The path the application took to
28 * [Fenix perf glossary](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary)
29 * for specific definitions.
31 enum class StartupPath {
36 * The start up path if we received an Intent but we're unable to categorize it into other buckets.
41 * The start up path has not been set. This state includes:
42 * - this API is accessed before it is set
43 * - if no intent is received before the activity is STARTED (e.g. app switcher)
49 * Returns the [StartupPath] for the currently started activity. This value will be set
50 * after an [Intent] is received that causes this activity to move into the STARTED state.
52 var startupPathForActivity = StartupPath.NOT_SET
55 private var wasResumedSinceStartedState = false
57 fun attachOnActivityOnCreate(lifecycle: Lifecycle, intent: Intent?) {
58 lifecycle.addObserver(StartupPathLifecycleObserver())
59 onIntentReceived(intent)
62 // N.B.: this method duplicates the actual logic for determining what page to open.
63 // Unfortunately, it's difficult to re-use that logic because it occurs in many places throughout
64 // the code so we do the simple thing for now and duplicate it. It's noticeably different from
65 // what you might expect: e.g. ACTION_MAIN can open a URL and if ACTION_VIEW provides an invalid
66 // URL, it'll perform a MAIN action. However, it's fairly representative of what users *intended*
67 // to do when opening the app and shouldn't change much because it's based on Android system-wide
68 // conventions, so it's probably fine for our purposes.
69 private fun getStartupPathFromIntent(intent: Intent): StartupPath = when (intent.action) {
70 Intent.ACTION_MAIN -> StartupPath.MAIN
71 Intent.ACTION_VIEW -> StartupPath.VIEW
72 else -> StartupPath.UNKNOWN
76 * Expected to be called when a new [Intent] is received by the [Activity]: i.e.
77 * [Activity.onCreate] and [Activity.onNewIntent].
79 fun onIntentReceived(intent: Intent?) {
80 // We want to set a path only if the intent causes the Activity to move into the STARTED state.
81 // This means we want to discard any intents that are received when the app is foregrounded.
82 // However, we can't use the Lifecycle.currentState to determine this because:
83 // - the app is briefly paused (state becomes STARTED) before receiving the Intent in
84 // the foreground so we can't say <= STARTED
85 // - onIntentReceived can be called from the CREATED or STARTED state so we can't say == CREATED
86 // So we're forced to track this state ourselves.
87 if (!wasResumedSinceStartedState && intent != null) {
88 startupPathForActivity = getStartupPathFromIntent(intent)
92 @VisibleForTesting(otherwise = NONE)
93 fun getTestCallbacks() = StartupPathLifecycleObserver()
95 @VisibleForTesting(otherwise = PRIVATE)
96 inner class StartupPathLifecycleObserver : DefaultLifecycleObserver {
97 override fun onResume(owner: LifecycleOwner) {
98 wasResumedSinceStartedState = true
101 override fun onStop(owner: LifecycleOwner) {
102 // Clear existing state.
103 startupPathForActivity = StartupPath.NOT_SET
104 wasResumedSinceStartedState = false