[fenix] For https://github.com/mozilla-mobile/fenix/issues/18836: add StartupPathProv...
[gecko.git] / mobile / android / fenix / app / src / main / java / org / mozilla / fenix / perf / StartupPathProvider.kt
blobf6b6f375f697c9b73226e9e036c5fbde39779989
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
16 /**
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.
20  *
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.
23  */
24 class StartupPathProvider {
26     /**
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.
30      */
31     enum class StartupPath {
32         MAIN,
33         VIEW,
35         /**
36          * The start up path if we received an Intent but we're unable to categorize it into other buckets.
37          */
38         UNKNOWN,
40         /**
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)
44          */
45         NOT_SET
46     }
48     /**
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.
51      */
52     var startupPathForActivity = StartupPath.NOT_SET
53         private set
55     private var wasResumedSinceStartedState = false
57     fun attachOnActivityOnCreate(lifecycle: Lifecycle, intent: Intent?) {
58         lifecycle.addObserver(StartupPathLifecycleObserver())
59         onIntentReceived(intent)
60     }
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
73     }
75     /**
76      * Expected to be called when a new [Intent] is received by the [Activity]: i.e.
77      * [Activity.onCreate] and [Activity.onNewIntent].
78      */
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)
89         }
90     }
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
99         }
101         override fun onStop(owner: LifecycleOwner) {
102             // Clear existing state.
103             startupPathForActivity = StartupPath.NOT_SET
104             wasResumedSinceStartedState = false
105         }
106     }