forked from mozilla/glean
/
Glean.kt
860 lines (758 loc) · 33.5 KB
/
Glean.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/. */
package mozilla.telemetry.glean
import android.app.ActivityManager
import android.util.Log
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Process
import androidx.annotation.MainThread
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.ProcessLifecycleOwner
import com.sun.jna.StringArray
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import mozilla.telemetry.glean.config.Configuration
import mozilla.telemetry.glean.config.FfiConfiguration
import mozilla.telemetry.glean.utils.getLocaleTag
import java.io.File
import mozilla.telemetry.glean.rust.LibGleanFFI
import mozilla.telemetry.glean.rust.getAndConsumeRustString
import mozilla.telemetry.glean.rust.toBoolean
import mozilla.telemetry.glean.rust.toByte
import mozilla.telemetry.glean.GleanMetrics.GleanInternalMetrics
import mozilla.telemetry.glean.GleanMetrics.Pings
import mozilla.telemetry.glean.net.BaseUploader
import mozilla.telemetry.glean.private.PingTypeBase
import mozilla.telemetry.glean.private.RecordedExperimentData
import mozilla.telemetry.glean.scheduler.GleanLifecycleObserver
import mozilla.telemetry.glean.scheduler.PingUploadWorker
import mozilla.telemetry.glean.scheduler.MetricsPingScheduler
import mozilla.telemetry.glean.utils.ThreadUtils
import org.json.JSONObject
/**
* Public exported type identifying individual timers for
* [TimingDistributionMetricType][mozilla.telemetry.glean.private.TimingDistributionMetricType].
*/
data class GleanTimerId internal constructor(internal val id: Long)
/**
* The main Glean API.
*
* This is exposed through the global [Glean] object.
*/
@Suppress("TooManyFunctions")
open class GleanInternalAPI internal constructor () {
companion object {
private const val LOG_TAG: String = "glean/Glean"
private const val LANGUAGE_BINDING_NAME: String = "Kotlin"
internal const val GLEAN_DATA_DIR: String = "glean_data"
}
private var initialized: Boolean = false
// Set when `initialize()` returns.
// This allows to detect calls that happen before `Glean.initialize()` was called.
// Note: The initialization might still be in progress, as it runs in a separate thread.
private var initFinished: Boolean = false
internal lateinit var configuration: Configuration
// This is the wrapped http uploading mechanism: provides base functionalities
// for logging and delegates the actual upload to the implementation in
// the `Configuration`.
internal lateinit var httpClient: BaseUploader
private lateinit var applicationContext: Context
// Note: we set `applicationContext` early during startup so this should be fine.
private val gleanLifecycleObserver by lazy { GleanLifecycleObserver() }
private lateinit var gleanDataDir: File
// Keep track of this value before Glean is initialized
private var debugViewTag: String? = null
// Keep track of this value before Glean is initialized
private var logPings: Boolean = false
// Keep track of source tags if set before Glean is initialized.
private var sourceTags: Set<String>? = null
// This object holds data related to any persistent information about the metrics ping,
// such as the last time it was sent and the store name
internal lateinit var metricsPingScheduler: MetricsPingScheduler
// Keep track of ping types that have been registered before Glean is initialized.
internal val pingTypeQueue: MutableSet<PingTypeBase> = mutableSetOf()
// This is used to cache the process state and is used by the function `isMainProcess()`
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var isMainProcess: Boolean? = null
// When sending pings to a test endpoint, we're probably in instrumented tests. In that
// case pings are to be immediately submitted by the WorkManager.
internal var isSendingToTestEndpoint: Boolean = false
/**
* Initialize the Glean SDK.
*
* This should only be initialized once by the application, and not by
* libraries using the Glean SDK. A message is logged to error and no
* changes are made to the state if initialize is called a more than
* once.
*
* A LifecycleObserver will be added to send pings when the application goes
* into foreground and background.
*
* This method must be called from the main thread.
*
* @param applicationContext [Context] to access application features, such
* as shared preferences
* @param uploadEnabled A [Boolean] that determines whether telemetry is enabled.
* If disabled, all persisted metrics, events and queued pings (except
* first_run_date) are cleared.
* @param configuration A Glean [Configuration] object with global settings.
*/
@Suppress("ReturnCount", "LongMethod", "ComplexMethod")
@JvmOverloads
@Synchronized
@MainThread
fun initialize(
applicationContext: Context,
uploadEnabled: Boolean,
configuration: Configuration = Configuration()
) {
// Glean initialization must be called on the main thread, or lifecycle
// registration may fail. This is also enforced at build time by the
// @MainThread decorator, but this run time check is also performed to
// be extra certain.
ThreadUtils.assertOnUiThread()
// In certain situations Glean.initialize may be called from a process other than the main
// process. In this case we want initialize to be a no-op and just return.
if (!isMainProcess(applicationContext)) {
Log.e(LOG_TAG, "Attempted to initialize Glean on a process other than the main process")
return
}
if (isInitialized()) {
Log.e(LOG_TAG, "Glean should not be initialized multiple times")
return
}
this.applicationContext = applicationContext
this.configuration = configuration
this.httpClient = BaseUploader(configuration.httpClient)
this.gleanDataDir = File(applicationContext.applicationInfo.dataDir, GLEAN_DATA_DIR)
// Execute startup off the main thread.
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.executeTask {
val cfg = FfiConfiguration(
dataDir = gleanDataDir.path,
packageName = applicationContext.packageName,
languageBindingName = LANGUAGE_BINDING_NAME,
uploadEnabled = uploadEnabled,
maxEvents = configuration.maxEvents,
delayPingLifetimeIO = false
)
initialized = LibGleanFFI.INSTANCE.glean_initialize(cfg).toBoolean()
// If initialization of Glean fails we bail out and don't initialize further.
if (!initialized) {
return@executeTask
}
// The debug view tag might have been set before initialize,
// get the cached value and set it.
if (debugViewTag != null) {
setDebugViewTag(debugViewTag!!)
}
// The log pings debug option might have been set before initialize,
// get the cached value and set it.
if (logPings) {
setLogPings(logPings)
}
// The source tags might have been set before initialize,
// get the cached value and set them.
sourceTags?.let { setSourceTags(it) }
// Get the current value of the dirty flag so we know whether to
// send a dirty startup baseline ping below. Immediately set it to
// `false` so that dirty startup pings won't be sent if Glean
// initialization does not complete successfully. It is set to `true`
// again in the ON_START lifecycle event.
val isDirtyFlagSet = LibGleanFFI.INSTANCE.glean_is_dirty_flag_set().toBoolean()
LibGleanFFI.INSTANCE.glean_set_dirty_flag(false.toByte())
// Register builtin pings.
// Unfortunately we need to manually list them here to guarantee they are registered synchronously
// before we need them.
// We don't need to handle the deletion-request ping. It's never touched from the language implementation.
LibGleanFFI.INSTANCE.glean_register_ping_type(Pings.baseline.handle)
LibGleanFFI.INSTANCE.glean_register_ping_type(Pings.metrics.handle)
LibGleanFFI.INSTANCE.glean_register_ping_type(Pings.events.handle)
// If any pings were registered before initializing, do so now.
// We're not clearing this queue in case Glean is reset by tests.
synchronized(this@GleanInternalAPI) {
pingTypeQueue.forEach {
// We're registering pings synchronously here,
// as this whole `initialize` block already runs off the main thread.
LibGleanFFI.INSTANCE.glean_register_ping_type(it.handle)
}
}
// If this is the first time ever the Glean SDK runs, make sure to set
// some initial core metrics in case we need to generate early pings.
// The next times we start, we would have them around already.
val isFirstRun = LibGleanFFI.INSTANCE.glean_is_first_run().toBoolean()
if (isFirstRun) {
initializeCoreMetrics(applicationContext)
}
// Deal with any pending events so we can start recording new ones
val pingSubmitted = LibGleanFFI.INSTANCE.glean_on_ready_to_submit_pings().toBoolean()
// We need to enqueue the PingUploadWorker in these cases:
// 1. Pings were submitted through Glean and it is ready to upload those pings;
// 2. Upload is disabled, to upload a possible deletion-request ping.
if (pingSubmitted || !uploadEnabled) {
PingUploadWorker.enqueueWorker(applicationContext)
}
// Set up information and scheduling for Glean owned pings. Ideally, the "metrics"
// ping startup check should be performed before any other ping, since it relies
// on being dispatched to the API context before any other metric.
metricsPingScheduler = MetricsPingScheduler(applicationContext)
metricsPingScheduler.schedule()
// Check if the "dirty flag" is set. That means the product was probably
// force-closed. If that's the case, submit a 'baseline' ping with the
// reason "dirty_startup". We only do that from the second run.
if (!isFirstRun && isDirtyFlagSet) {
submitPingByNameSync("baseline", "dirty_startup")
}
// From the second time we run, after all startup pings are generated,
// make sure to clear `lifetime: application` metrics and set them again.
// Any new value will be sent in newly generated pings after startup.
if (!isFirstRun) {
LibGleanFFI.INSTANCE.glean_clear_application_lifetime_metrics()
initializeCoreMetrics(applicationContext)
}
// Signal Dispatcher that init is complete
Dispatchers.API.flushQueuedInitialTasks()
// At this point, all metrics and events can be recorded.
// This should only be called from the main thread. This is enforced by
// the @MainThread decorator and the `assertOnUiThread` call.
MainScope().launch {
ProcessLifecycleOwner.get().lifecycle.addObserver(gleanLifecycleObserver)
}
}
this.initFinished = true
}
/**
* Returns true if the Glean SDK has been initialized.
*/
internal fun isInitialized(): Boolean {
return initialized
}
/**
* Register the pings generated from `pings.yaml` with the Glean SDK.
*
* @param pings The `Pings` object generated for your library or application
* by the Glean SDK.
*/
fun registerPings(pings: Any) {
// Instantiating the Pings object to send this function is enough to
// call the constructor and have it registered through [Glean.registerPingType].
Log.i(LOG_TAG, "Registering pings for ${pings.javaClass.canonicalName}")
}
/**
* Enable or disable Glean collection and upload.
*
* Metric collection is enabled by default.
*
* When uploading is disabled, metrics aren't recorded at all and no data
* is uploaded.
*
* When disabling, all pending metrics, events and queued pings are cleared.
*
* When enabling, the core Glean metrics are recreated.
*
* @param enabled When true, enable metric collection.
*/
fun setUploadEnabled(enabled: Boolean) {
if (!this.initFinished) {
val msg = """
Changing upload enabled before Glean is initialized is not supported.
Pass the correct state into `Glean.initialize()`.
See documentation at https://mozilla.github.io/glean/book/user/general-api.html#initializing-the-glean-sdk
""".trimIndent()
Log.e(LOG_TAG, msg)
return
}
// Changing upload enabled always happens asynchronous.
// That way it follows what a user expect when calling it inbetween other calls:
// It executes in the right order.
//
// Because the dispatch queue is halted until Glean is fully initialized
// we can safely enqueue here and it will execute after initialization.
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.launch {
val originalEnabled = internalGetUploadEnabled()
LibGleanFFI.INSTANCE.glean_set_upload_enabled(enabled.toByte())
if (!enabled) {
// Cancel any pending workers here so that we don't accidentally upload or
// collect data after the upload has been disabled.
metricsPingScheduler.cancel()
// Cancel any pending workers here so that we don't accidentally upload
// data after the upload has been disabled.
PingUploadWorker.cancel(applicationContext)
}
if (!originalEnabled && enabled) {
// If uploading is being re-enabled, we have to restore the
// application-lifetime metrics.
initializeCoreMetrics((this@GleanInternalAPI).applicationContext)
}
if (originalEnabled && !enabled) {
// If uploading is disabled, we need to send the deletion-request ping
PingUploadWorker.enqueueWorker(applicationContext)
}
}
}
/**
* Get whether or not Glean is allowed to record and upload data.
*
* Caution: the result is only correct if Glean is already initialized.
*
* **THIS METHOD IS DEPRECATED.**
* Applications should not rely on Glean's internal state.
* Upload enabled status should be tracked by the application and communicated to Glean if it changes.
*/
@Deprecated("Upload enabled should be tracked by the application and communicated to Glean if it changes")
fun getUploadEnabled(): Boolean {
return internalGetUploadEnabled()
}
/**
* Get whether or not Glean is allowed to record and upload data.
*
* Caution: the result is only correct if Glean is already initialized.
*
* Note: due to the deprecation notice and because warnings break the build,
* we pull out the implementation into an internal method.
*/
internal fun internalGetUploadEnabled(): Boolean {
if (isInitialized()) {
return LibGleanFFI.INSTANCE.glean_is_upload_enabled().toBoolean()
} else {
return false
}
}
/**
* Indicate that an experiment is running. Glean will then add an
* experiment annotation to the environment which is sent with pings. This
* information is not persisted between runs.
*
* @param experimentId The id of the active experiment (maximum 100 bytes)
* @param branch The experiment branch (maximum 100 bytes)
* @param extra Optional metadata to output with the ping
*/
@JvmOverloads
fun setExperimentActive(
experimentId: String,
branch: String,
extra: Map<String, String>? = null
) {
// The Map is sent over FFI as a pair of arrays, one containing the
// keys, and the other containing the values.
// In Kotlin, Map.keys and Map.values are not guaranteed to return the entries
// in any particular order. Therefore, we iterate over the pairs together and
// create the keys and values arrays step-by-step.
var keys: StringArray? = null
var values: StringArray? = null
var numKeys = 0
extra?.let {
numKeys = extra.size
val extraList = extra.toList()
keys = StringArray(Array(extra.size) { extraList[it].first }, "utf-8")
values = StringArray(Array(extra.size) { extraList[it].second }, "utf-8")
}
// We dispatch this asynchronously so that, if called before the Glean SDK is
// initialized, it doesn't get ignored and will be replayed after init.
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.launch {
LibGleanFFI.INSTANCE.glean_set_experiment_active(
experimentId,
branch,
keys,
values,
numKeys
)
}
}
/**
* Indicate that an experiment is no longer running.
*
* @param experimentId The id of the experiment to deactivate.
*/
fun setExperimentInactive(experimentId: String) {
// We dispatch this asynchronously so that, if called before the Glean SDK is
// initialized, it doesn't get ignored and will be replayed after init.
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.launch {
LibGleanFFI.INSTANCE.glean_set_experiment_inactive(experimentId)
}
}
/**
* Tests whether an experiment is active, for testing purposes only.
*
* @param experimentId the id of the experiment to look for.
* @return true if the experiment is active and reported in pings, otherwise false
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun testIsExperimentActive(experimentId: String): Boolean {
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.assertInTestingMode()
return LibGleanFFI.INSTANCE.glean_experiment_test_is_active(experimentId).toBoolean()
}
/**
* Utility function to get a String -> String [Map] out of a [JSONObject].
*/
private fun getMapFromJSONObject(jsonRes: JSONObject): Map<String, String>? {
return jsonRes.optJSONObject("extra")?.let {
val map = mutableMapOf<String, String>()
it.names()?.let { names ->
for (i in 0 until names.length()) {
map[names.getString(i)] = it.getString(names.getString(i))
}
}
map
}
}
/**
* Returns the stored data for the requested active experiment, for testing purposes only.
*
* @param experimentId the id of the experiment to look for.
* @return the [RecordedExperimentData] for the experiment
* @throws [NullPointerException] if the requested experiment is not active or data is corrupt.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun testGetExperimentData(experimentId: String): RecordedExperimentData {
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.assertInTestingMode()
val ptr = LibGleanFFI.INSTANCE.glean_experiment_test_get_data(
experimentId
)!!
var branchId: String
var extraMap: Map<String, String>?
try {
// Parse and extract the fields from the JSON string here so
// that we can always throw NullPointerException if something
// goes wrong.
val jsonRes = JSONObject(ptr.getAndConsumeRustString())
branchId = jsonRes.getString("branch")
extraMap = getMapFromJSONObject(jsonRes)
} catch (e: org.json.JSONException) {
throw NullPointerException()
}
return RecordedExperimentData(branchId, extraMap)
}
/**
* Initialize the core metrics internally managed by Glean (e.g. client id).
*/
private fun initializeCoreMetrics(applicationContext: Context) {
// Set a few more metrics that will be sent as part of every ping.
// Please note that the following metrics must be set synchronously, so
// that they are guaranteed to be available with the first ping that is
// generated. We use an internal only API to do that.
// https://developer.android.com/reference/android/os/Build.VERSION
GleanInternalMetrics.androidSdkVersion.setSync(Build.VERSION.SDK_INT.toString())
GleanInternalMetrics.osVersion.setSync(Build.VERSION.RELEASE)
// https://developer.android.com/reference/android/os/Build
GleanInternalMetrics.deviceManufacturer.setSync(Build.MANUFACTURER)
GleanInternalMetrics.deviceModel.setSync(Build.MODEL)
GleanInternalMetrics.architecture.setSync(Build.SUPPORTED_ABIS[0])
GleanInternalMetrics.locale.setSync(getLocaleTag())
configuration.channel?.let {
GleanInternalMetrics.appChannel.setSync(it)
}
val packageInfo: PackageInfo
try {
packageInfo = applicationContext.packageManager.getPackageInfo(
applicationContext.packageName, 0
)
} catch (e: PackageManager.NameNotFoundException) {
Log.e(
LOG_TAG,
"Could not get own package info, unable to report build id and display version"
)
GleanInternalMetrics.appBuild.setSync("inaccessible")
GleanInternalMetrics.appDisplayVersion.setSync("inaccessible")
return
}
@Suppress("DEPRECATION")
GleanInternalMetrics.appBuild.setSync(packageInfo.versionCode.toString())
GleanInternalMetrics.appDisplayVersion.setSync(
packageInfo.versionName?.let { it } ?: "Unknown"
)
}
/**
* Get the data directory for Glean.
*/
internal fun getDataDir(): File {
return this.gleanDataDir
}
/**
* Collect a ping and return a string
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
internal fun testCollect(ping: PingTypeBase, reason: String? = null): String? {
return LibGleanFFI.INSTANCE.glean_ping_collect(ping.handle, reason)?.getAndConsumeRustString()
}
/**
* Handle the foreground event and send the appropriate pings.
*/
internal fun handleForegroundEvent() {
Pings.baseline.submit(Pings.baselineReasonCodes.foreground)
}
/**
* Handle the background event and send the appropriate pings.
*/
internal fun handleBackgroundEvent() {
Pings.baseline.submit(Pings.baselineReasonCodes.background)
Pings.events.submit(Pings.eventsReasonCodes.background)
}
/**
* Collect and submit a ping for eventual upload.
*
* The ping content is assembled as soon as possible, but upload is not
* guaranteed to happen immediately, as that depends on the upload
* policies.
*
* If the ping currently contains no content, it will not be assembled and
* queued for sending.
*
* @param ping Ping to submit.
* @param reason The reason the ping is being submitted.
* @return The async [Job] performing the work of assembling the ping
*/
internal fun submitPing(ping: PingTypeBase, reason: String? = null): Job? {
return submitPingByName(ping.name, reason)
}
/**
* Collect and submit a ping for eventual upload by name.
*
* The ping will be looked up in the known instances of [PingType]. If the
* ping isn't known, an error is logged and the ping isn't queued for uploading.
*
* The ping content is assembled as soon as possible, but upload is not
* guaranteed to happen immediately, as that depends on the upload
* policies.
*
* If the ping currently contains no content, it will not be assembled and
* queued for sending, unless explicitly specified otherwise in the registry
* file.
*
* @param pingName Name of the ping to submit.
* @param reason The reason the ping is being submitted.
* @return The async [Job] performing the work of assembling the ping
*/
@Suppress("EXPERIMENTAL_API_USAGE")
internal fun submitPingByName(pingName: String, reason: String? = null) = Dispatchers.API.launch {
submitPingByNameSync(pingName, reason)
}
/**
* Collect and submit a ping (by its name) for eventual upload, synchronously.
*
* The ping will be looked up in the known instances of [PingType]. If the
* ping isn't known, an error is logged and the ping isn't queued for uploading.
*
* The ping content is assembled as soon as possible, but upload is not
* guaranteed to happen immediately, as that depends on the upload
* policies.
*
* If the ping currently contains no content, it will not be assembled and
* queued for sending, unless explicitly specified otherwise in the registry
* file.
*
* @param pingName Name of the ping to submit.
* @param reason The reason the ping is being submitted.
*/
internal fun submitPingByNameSync(pingName: String, reason: String? = null) {
if (!isInitialized()) {
Log.e(LOG_TAG, "Glean must be initialized before submitting pings.")
return
}
if (!internalGetUploadEnabled()) {
Log.i(LOG_TAG, "Glean disabled: not submitting any pings.")
return
}
val submittedPing = LibGleanFFI.INSTANCE.glean_submit_ping_by_name(
pingName,
reason
).toBoolean()
if (submittedPing) {
PingUploadWorker.enqueueWorker(applicationContext)
}
}
/**
* Set a tag to be applied to headers when uploading pings for debug view.
*
* If the tag is invalid it won't be set and this function will return `false`,
* although if we are not initialized yet, there won't be any validation.
*
* This is only meant to be used internally by the `GleanDebugActivity`.
*
* @param value The value of the tag, which must be a valid HTTP header value.
*/
internal fun setDebugViewTag(value: String): Boolean {
if (isInitialized()) {
return LibGleanFFI.INSTANCE.glean_set_debug_view_tag(value).toBoolean()
} else {
debugViewTag = value
// When setting the debug view tag before initialization,
// we don't validate the tag, thus this function always returns true.
return true
}
}
/**
* Set the source tags to be applied as headers when uploading pings.
*
* If any of the tags is invalid nothing will be set and this function will
* return `false`, although if we are not initialized yet, there won't be any validation.
*
* This is only meant to be used internally by the `GleanDebugActivity`.
*
* @param tags A list of tags, which must be valid HTTP header values.
*/
internal fun setSourceTags(tags: Set<String>): Boolean {
return if (isInitialized()) {
val tagList = StringArray(tags.toList().toTypedArray(), "utf-8")
LibGleanFFI.INSTANCE.glean_set_source_tags(tagList, tags.size).toBoolean()
} else {
sourceTags = tags
// When setting the source tags before initialization,
// we don't validate the tags, thus this function always returns true.
true
}
}
/**
* Set the logPing debug option, when this is `true`
* the payload of assembled ping requests get logged.
*
* This is only meant to be used internally by the `GleanDebugActivity`.
*
* @param value The value of the option.
*/
internal fun setLogPings(value: Boolean) {
if (isInitialized()) {
return LibGleanFFI.INSTANCE.glean_set_log_pings(value.toByte())
} else {
logPings = value
}
}
/**
* TEST ONLY FUNCTION.
* This is called by the GleanTestRule, to enable test mode.
*
* This makes all asynchronous work synchronous so we can test the results of the
* API synchronously.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
internal fun enableTestingMode() {
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.setTestingMode(enabled = true)
}
/**
* TEST ONLY FUNCTION.
* Resets the Glean state and trigger init again.
*
* @param context the application context to init Glean with
* @param config the [Configuration] to init Glean with
* @param clearStores if true, clear the contents of all stores
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
internal fun resetGlean(
context: Context,
config: Configuration,
clearStores: Boolean,
uploadEnabled: Boolean = true
) {
Glean.enableTestingMode()
if (isInitialized() && clearStores) {
// Clear all the stored data.
LibGleanFFI.INSTANCE.glean_test_clear_all_stores()
}
isMainProcess = null
// Init Glean.
Glean.testDestroyGleanHandle()
// Always log pings for tests
Glean.setLogPings(true)
Glean.initialize(context, uploadEnabled, config)
}
/**
* TEST ONLY FUNCTION.
* Sets the server endpoint to a local address for ingesting test pings.
*
* The endpoint will be set as "http://localhost:<port>" and pings will be
* immediately sent by the WorkManager.
*
* @param port the local address to send pings to
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
internal fun testSetLocalEndpoint(port: Int) {
Glean.enableTestingMode()
isSendingToTestEndpoint = true
// We can't set the configuration unless we're initialized.
assert(isInitialized())
val endpointUrl = "http://localhost:$port"
Glean.configuration = configuration.copy(serverEndpoint = endpointUrl)
}
/**
* Test-only method to destroy the owned glean-core handle.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
internal fun testDestroyGleanHandle() {
if (!isInitialized()) {
// We don't need to destroy Glean: it wasn't initialized.
return
}
LibGleanFFI.INSTANCE.glean_destroy_glean()
// Reset all state.
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.setTaskQueueing(true)
initFinished = false
initialized = false
}
/**
* Register a [PingType] in the registry associated with this [Glean] object.
*/
@Synchronized
internal fun registerPingType(pingType: PingTypeBase) {
// If this happens after Glean.initialize is called (and returns),
// we dispatch ping registration on the thread pool.
// Registering a ping should not block the application.
// Submission itself is also dispatched, so it will always come after the registration.
if (this.initFinished) {
@Suppress("EXPERIMENTAL_API_USAGE")
Dispatchers.API.launch {
LibGleanFFI.INSTANCE.glean_register_ping_type(
pingType.handle
)
}
}
// We need to keep track of pings, so they get re-registered after a reset.
// This state is kept across Glean resets, which should only ever happen in test mode.
// Or by the instrumentation tests (`connectedAndroidTest`), which relaunches the application activity,
// but not the whole process, meaning globals, such as the ping types, still exist from the old run.
// It's a set and keeping them around forever should not have much of an impact.
pingTypeQueue.add(pingType)
}
/**
* Returns true if a ping by this name is in the ping registry.
*
* For internal testing only.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
internal fun testHasPingType(pingName: String): Boolean {
return LibGleanFFI.INSTANCE.glean_test_has_ping_type(pingName).toBoolean()
}
/**
* Returns true if we are running in the main process false otherwise.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun isMainProcess(context: Context): Boolean {
if (isMainProcess != null) return isMainProcess as Boolean
val pid = Process.myPid()
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
isMainProcess = (activityManager.runningAppProcesses?.any { processInfo ->
(processInfo.pid == pid && processInfo.processName == context.packageName)
}) ?: false
return isMainProcess as Boolean
}
}
/**
* The main Glean object.
*
* ```
* Glean.setUploadEnabled(true)
* Glean.initialize(applicationContext)
* ```
*/
object Glean : GleanInternalAPI()