• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

snowplow / snowplow-android-tracker / #1049

pending completion
#1049

push

github-actions

matus-tomlein
Return empty string from SessionController.getSessionId in case session not initialized

5 of 5 new or added lines in 1 file covered. (100.0%)

3483 of 4576 relevant lines covered (76.11%)

0.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.01
/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/internal/tracker/Tracker.java
1
/*
2
 * Copyright (c) 2015-2022 Snowplow Analytics Ltd. All rights reserved.
3
 *
4
 * This program is licensed to you under the Apache License Version 2.0,
5
 * and you may not use this file except in compliance with the Apache License Version 2.0.
6
 * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
7
 *
8
 * Unless required by applicable law or agreed to in writing,
9
 * software distributed under the Apache License Version 2.0 is distributed on an
10
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
 * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
12
 */
13

14
package com.snowplowanalytics.snowplow.internal.tracker;
15

16
import android.content.Context;
17

18
import androidx.annotation.Nullable;
19
import androidx.annotation.NonNull;
20

21
import java.util.Collections;
22
import java.util.HashMap;
23
import java.util.LinkedList;
24
import java.util.List;
25
import java.util.Map;
26
import java.util.Objects;
27
import java.util.Set;
28
import java.util.UUID;
29
import java.util.concurrent.TimeUnit;
30
import java.util.concurrent.atomic.AtomicBoolean;
31

32
import com.snowplowanalytics.snowplow.entity.DeepLink;
33
import com.snowplowanalytics.snowplow.event.Background;
34
import com.snowplowanalytics.snowplow.event.DeepLinkReceived;
35
import com.snowplowanalytics.snowplow.event.Foreground;
36
import com.snowplowanalytics.snowplow.internal.utils.NotificationCenter;
37
import com.snowplowanalytics.snowplow.tracker.BuildConfig;
38
import com.snowplowanalytics.snowplow.tracker.DevicePlatform;
39
import com.snowplowanalytics.snowplow.internal.emitter.Emitter;
40
import com.snowplowanalytics.snowplow.internal.emitter.Executor;
41
import com.snowplowanalytics.snowplow.internal.gdpr.Gdpr;
42
import com.snowplowanalytics.snowplow.tracker.InspectableEvent;
43
import com.snowplowanalytics.snowplow.tracker.LoggerDelegate;
44
import com.snowplowanalytics.snowplow.internal.session.Session;
45
import com.snowplowanalytics.snowplow.internal.constants.TrackerConstants;
46
import com.snowplowanalytics.snowplow.internal.constants.Parameters;
47
import com.snowplowanalytics.snowplow.globalcontexts.GlobalContext;
48
import com.snowplowanalytics.snowplow.event.Event;
49
import com.snowplowanalytics.snowplow.event.TrackerError;
50
import com.snowplowanalytics.snowplow.payload.Payload;
51
import com.snowplowanalytics.snowplow.payload.TrackerPayload;
52
import com.snowplowanalytics.snowplow.internal.session.ProcessObserver;
53
import com.snowplowanalytics.snowplow.tracker.LogLevel;
54
import com.snowplowanalytics.snowplow.payload.SelfDescribingJson;
55
import com.snowplowanalytics.snowplow.internal.utils.Util;
56
import com.snowplowanalytics.snowplow.util.Basis;
57

58

59
/**
60
 * Builds a Tracker object which is used to send events to a Snowplow Collector.
61
 */
62
public class Tracker {
63

64
    /**
65
     * Builder for the Tracker
66
     */
67
    public static class TrackerBuilder {
68

69
        final @NonNull Emitter emitter; // Required
70
        final @NonNull String namespace; // Required
71
        final @NonNull String appId; // Required
72
        final @NonNull Context context; // Required
73
        @Nullable Subject subject = null; // Optional
1✔
74
        boolean base64Encoded = true; // Optional
1✔
75
        @Nullable
1✔
76
        DevicePlatform devicePlatform = DevicePlatform.Mobile; // Optional
77
        LogLevel logLevel = LogLevel.OFF; // Optional
1✔
78
        boolean sessionContext = false; // Optional
1✔
79
        long foregroundTimeout = 1800; // Optional - 30 minutes
1✔
80
        long backgroundTimeout = 1800; // Optional - 30 minutes
1✔
81
        @NonNull Runnable[] sessionCallbacks = new Runnable[]{}; // Optional
1✔
82
        int threadCount = 10; // Optional
1✔
83
        TimeUnit timeUnit = TimeUnit.SECONDS; // Optional
1✔
84
        boolean geoLocationContext = false; // Optional
1✔
85
        boolean mobileContext = false; // Optional
1✔
86
        boolean applicationCrash = true; // Optional
1✔
87
        boolean trackerDiagnostic = false; // Optional
1✔
88
        boolean lifecycleEvents = false; // Optional
1✔
89
        boolean deepLinkContext = true; // Optional
1✔
90
        boolean screenContext = false; // Optional
1✔
91
        boolean activityTracking = false; // Optional
1✔
92
        boolean installTracking = false; // Optional
1✔
93
        boolean applicationContext = false; // Optional
1✔
94
        boolean userAnonymisation = false; // Optional
1✔
95
        @Nullable Gdpr gdpr = null; // Optional
1✔
96
        @Nullable String trackerVersionSuffix = null; // Optional
1✔
97

98
        /**
99
         * @param emitter Emitter to which events will be sent
100
         * @param namespace Identifier for the Tracker instance
101
         * @param appId Application ID
102
         * @param context The Android application context
103
         */
104
        public TrackerBuilder(@NonNull Emitter emitter, @NonNull String namespace, @NonNull String appId, @NonNull Context context) {
1✔
105
            this.emitter = emitter;
1✔
106
            this.namespace = namespace;
1✔
107
            this.appId = appId;
1✔
108
            this.context = context;
1✔
109
        }
1✔
110

111
        /**
112
         * @param isEnabled Whether application contexts are sent with all events
113
         * @return itself
114
         */
115
        @NonNull
116
        public TrackerBuilder applicationContext(boolean isEnabled) {
117
            this.applicationContext = isEnabled;
1✔
118
            return this;
1✔
119
        }
120

121
        /**
122
         * Enables GDPR context to be sent with every event.
123
         * @param basisForProcessing GDPR Basis for processing
124
         * @param documentId ID of a GDPR basis document
125
         * @param documentVersion Version of the document
126
         * @param documentDescription Description of the document
127
         * @return itself
128
         */
129
        @NonNull
130
        public TrackerBuilder gdprContext(@NonNull Basis basisForProcessing, @Nullable String documentId, @Nullable String documentVersion, @Nullable String documentDescription) {
131
            this.gdpr = new Gdpr(basisForProcessing, documentId, documentVersion, documentDescription);
1✔
132
            return this;
1✔
133
        }
134

135
        /**
136
         * @param willTrack Whether install events will be tracked
137
         * @return itself
138
         */
139
        @NonNull
140
        public TrackerBuilder installTracking(boolean willTrack) {
141
            this.installTracking = willTrack;
1✔
142
            return this;
1✔
143
        }
144

145
        /**
146
         * @param subject Subject to be tracked
147
         * @return itself
148
         */
149
        @NonNull
150
        public TrackerBuilder subject(@Nullable Subject subject) {
151
            this.subject = subject;
1✔
152
            return this;
1✔
153
        }
154

155
        /**
156
         * @param base64 Whether JSONs in the payload should be base-64 encoded
157
         * @return itself
158
         */
159
        @NonNull
160
        public TrackerBuilder base64(@Nullable Boolean base64) {
161
            this.base64Encoded = base64;
1✔
162
            return this;
1✔
163
        }
164

165
        /**
166
         * @param platform The device platform the tracker is running on
167
         * @return itself
168
         */
169
        @NonNull
170
        public TrackerBuilder platform(@Nullable DevicePlatform platform) {
171
            this.devicePlatform = platform;
1✔
172
            return this;
1✔
173
        }
174

175
        /**
176
         * @param log The log level for the Tracker class
177
         * @return itself
178
         */
179
        @NonNull
180
        public TrackerBuilder level(@Nullable LogLevel log) {
181
            this.logLevel = log;
1✔
182
            return this;
1✔
183
        }
184

185
        /**
186
         * @param delegate The logger delegate that receive logs from the tracker.
187
         * @return itself
188
         */
189
        @NonNull
190
        public TrackerBuilder loggerDelegate(@Nullable LoggerDelegate delegate) {
191
            Logger.setDelegate(delegate);
1✔
192
            return this;
1✔
193
        }
194

195
        /**
196
         * @param sessionContext whether to add a session context
197
         * @return itself
198
         */
199
        @NonNull
200
        public TrackerBuilder sessionContext(boolean sessionContext) {
201
            this.sessionContext = sessionContext;
1✔
202
            return this;
1✔
203
        }
204

205
        /**
206
         * @param timeout The session foreground timeout
207
         * @return itself
208
         */
209
        @NonNull
210
        public TrackerBuilder foregroundTimeout(long timeout) {
211
            this.foregroundTimeout = timeout;
1✔
212
            return this;
1✔
213
        }
214

215
        /**
216
         * @param timeout The session background timeout
217
         * @return itself
218
         */
219
        @NonNull
220
        public TrackerBuilder backgroundTimeout(long timeout) {
221
            this.backgroundTimeout = timeout;
1✔
222
            return this;
1✔
223
        }
224

225
        /**
226
         * @param foregroundTransitionCallback Called when session transitions to foreground
227
         * @param backgroundTransitionCallback Called when session transitions to background
228
         * @param foregroundTimeoutCallback Called when foregrounded session times-out
229
         * @param backgroundTimeoutCallback Called when backgrounded session times-out
230
         * @return itself
231
         */
232
        @NonNull
233
        public TrackerBuilder sessionCallbacks(@NonNull Runnable foregroundTransitionCallback,
234
                                               @NonNull Runnable backgroundTransitionCallback,
235
                                               @NonNull Runnable foregroundTimeoutCallback,
236
                                               @NonNull Runnable backgroundTimeoutCallback)
237
        {
238
            this.sessionCallbacks = new Runnable[]{
×
239
                    foregroundTransitionCallback, backgroundTransitionCallback,
240
                    foregroundTimeoutCallback, backgroundTimeoutCallback
241
            };
242
            return this;
×
243
        }
244

245
        /**
246
         * @param threadCount the amount of threads to use for concurrency
247
         * @return itself
248
         */
249
        @NonNull
250
        public TrackerBuilder threadCount(int threadCount) {
251
            this.threadCount = threadCount;
1✔
252
            return this;
1✔
253
        }
254

255
        /**
256
         * @param timeUnit a valid TimeUnit
257
         * @return itself
258
         */
259
        @NonNull
260
        public TrackerBuilder timeUnit(@Nullable TimeUnit timeUnit) {
261
            this.timeUnit = timeUnit;
1✔
262
            return this;
1✔
263
        }
264

265
        /**
266
         * @param geoLocationContext whether to add a geo-location context
267
         * @return itself
268
         */
269
        @NonNull
270
        public TrackerBuilder geoLocationContext(@NonNull Boolean geoLocationContext) {
271
            this.geoLocationContext = geoLocationContext;
1✔
272
            return this;
1✔
273
        }
274

275
        /**
276
         * @param mobileContext whether to add a mobile context
277
         * @return itself
278
         */
279
        @NonNull
280
        public TrackerBuilder mobileContext(@NonNull Boolean mobileContext) {
281
            this.mobileContext = mobileContext;
1✔
282
            return this;
1✔
283
        }
284

285
        /**
286
         * @param applicationCrash whether to automatically track application crashes
287
         * @return itself
288
         */
289
        @NonNull
290
        public TrackerBuilder applicationCrash(@NonNull Boolean applicationCrash) {
291
            this.applicationCrash = applicationCrash;
1✔
292
            return this;
1✔
293
        }
294

295
        /**
296
         * @param trackerDiagnostic whether to automatically track error within the tracker.
297
         * @return itself
298
         */
299
        @NonNull
300
        public TrackerBuilder trackerDiagnostic(@NonNull Boolean trackerDiagnostic) {
301
            this.trackerDiagnostic = trackerDiagnostic;
1✔
302
            return this;
1✔
303
        }
304

305
        /**
306
         * @param lifecycleEvents whether to automatically track transition
307
         *                        from foreground to background
308
         * @return itself
309
         */
310
        @NonNull
311
        public TrackerBuilder lifecycleEvents(@NonNull Boolean lifecycleEvents) {
312
            this.lifecycleEvents = lifecycleEvents;
1✔
313
            return this;
1✔
314
        }
315

316
        @NonNull
317
        public TrackerBuilder deepLinkContext(@NonNull Boolean deepLinkContext) {
318
            this.deepLinkContext = deepLinkContext;
1✔
319
            return this;
1✔
320
        }
321

322
        /**
323
         * @param screenContext whether to send a screen context (info pertaining
324
         *                      to current screen) with every event
325
         * @return itself
326
         */
327
        @NonNull
328
        public synchronized TrackerBuilder screenContext(@NonNull Boolean screenContext) {
329
            this.screenContext = screenContext;
1✔
330
            return this;
1✔
331
        }
332

333
        /**
334
         * @param screenviewEvents whether to auto-track screenviews
335
         * @return itself
336
         */
337
        @NonNull
338
        public TrackerBuilder screenviewEvents(@NonNull Boolean screenviewEvents) {
339
            this.activityTracking = screenviewEvents;
1✔
340
            return this;
1✔
341
        }
342

343
        /**
344
         * @param userAnonymisation whether to anonymise client-side user identifiers in session (userId, previousSessionId), subject (userId, networkUserId, domainUserId, ipAddress) and platform context entities (IDFA)
345
         * @return itself
346
         */
347
        @NonNull
348
        public TrackerBuilder userAnonymisation(@NonNull Boolean userAnonymisation) {
349
            boolean changedUserAnonymisation = this.userAnonymisation != userAnonymisation;
1✔
350
            this.userAnonymisation = userAnonymisation;
1✔
351
            return this;
1✔
352
        }
353

354
        /**
355
         * Internal use only.
356
         * Decorate the `tv` (tracker version) field in the tracker protocol.
357
         */
358
        @NonNull
359
        public TrackerBuilder trackerVersionSuffix(@Nullable String trackerVersionSuffix) {
360
            this.trackerVersionSuffix = trackerVersionSuffix;
1✔
361
            return this;
1✔
362
        }
363
    }
364

365
    // ----
366

367
    private final static String TAG = Tracker.class.getSimpleName();
1✔
368
    private String trackerVersion = BuildConfig.TRACKER_LABEL;
1✔
369

370
    // --- Builder
371

372
    final Context context;
373
    Emitter emitter;
374
    Subject subject;
375
    Session trackerSession;
376
    String namespace;
377
    String appId;
378
    boolean base64Encoded;
379
    DevicePlatform devicePlatform;
380
    LogLevel level;
381
    private boolean sessionContext;
382
    Runnable[] sessionCallbacks;
383
    int threadCount;
384
    boolean geoLocationContext;
385
    boolean mobileContext;
386
    boolean applicationCrash;
387
    boolean trackerDiagnostic;
388
    boolean lifecycleEvents;
389
    boolean installTracking;
390
    boolean activityTracking;
391
    boolean applicationContext;
392
    private boolean userAnonymisation;
393
    String trackerVersionSuffix;
394

395
    private boolean deepLinkContext;
396
    private boolean screenContext;
397

398
    private Gdpr gdpr;
399
    private final StateManager stateManager;
400

401
    private final TimeUnit timeUnit;
402
    private final long foregroundTimeout;
403
    private final long backgroundTimeout;
404

405
    @NonNull
406
    private final PlatformContext platformContext;
407

408
    private final Map<String, GlobalContext> globalContextGenerators = Collections.synchronizedMap(new HashMap<>());
1✔
409

410
    private final NotificationCenter.FunctionalObserver receiveLifecycleNotification = new NotificationCenter.FunctionalObserver() {
1✔
411
        @Override
412
        public void apply(@NonNull Map<String, Object> data) {
413
            Session session = getSession();
1✔
414
            if (session == null || !lifecycleEvents) {
1✔
415
                return;
1✔
416
            }
417
            Boolean isForeground = (Boolean) data.get("isForeground");
1✔
418
            if (isForeground == null) {
1✔
419
                return;
×
420
            }
421
            if (session.isBackground() == !isForeground) {
1✔
422
                // if the new lifecycle state confirms the session state, there isn't any lifecycle transition
423
                return;
×
424
            }
425
            if (isForeground) {
1✔
426
                track(new Foreground().foregroundIndex(session.getForegroundIndex() + 1));
1✔
427
            } else {
428
                track(new Background().backgroundIndex(session.getBackgroundIndex() + 1));
1✔
429
            }
430
            session.setBackground(!isForeground);
1✔
431
        }
1✔
432
    };
433
    private final NotificationCenter.FunctionalObserver receiveScreenViewNotification = new NotificationCenter.FunctionalObserver() {
1✔
434
        @Override
435
        public void apply(@NonNull Map<String, Object> data) {
436
            if (activityTracking) {
×
437
                Event event = (Event) data.get("event");
×
438
                if (event != null) {
×
439
                    track(event);
×
440
                }
441
            }
442
        }
×
443
    };
444
    private final NotificationCenter.FunctionalObserver receiveInstallNotification = new NotificationCenter.FunctionalObserver() {
1✔
445
        @Override
446
        public void apply(@NonNull Map<String, Object> data) {
447
            if (installTracking) {
1✔
448
                Event event = (Event) data.get("event");
1✔
449
                if (event != null) {
1✔
450
                    track(event);
1✔
451
                }
452
            }
453
        }
1✔
454
    };
455
    private final NotificationCenter.FunctionalObserver receiveDiagnosticNotification = new NotificationCenter.FunctionalObserver() {
1✔
456
        @Override
457
        public void apply(@NonNull Map<String, Object> data) {
458
            if (trackerDiagnostic) {
1✔
459
                Event event = (Event) data.get("event");
×
460
                if (event != null) {
×
461
                    track(event);
×
462
                }
463
            }
464
        }
1✔
465
    };
466
    private final NotificationCenter.FunctionalObserver receiveCrashReportingNotification = new NotificationCenter.FunctionalObserver() {
1✔
467
        @Override
468
        public void apply(@NonNull Map<String, Object> data) {
469
            if (applicationCrash) {
1✔
470
                Event event = (Event) data.get("event");
×
471
                if (event != null) {
×
472
                    track(event);
×
473
                }
474
            }
475
        }
1✔
476
    };
477

478
    AtomicBoolean dataCollection = new AtomicBoolean(true);
1✔
479

480
    /**
481
     * Creates a new Snowplow Tracker.
482
     * @param builder The builder that constructs a tracker
483
     */
484
    public Tracker(@NonNull TrackerBuilder builder) {
1✔
485
        this.stateManager = new StateManager();
1✔
486
        this.context = builder.context;
1✔
487

488
        this.emitter = builder.emitter;
1✔
489
        this.emitter.flush();
1✔
490

491
        this.namespace = builder.namespace;
1✔
492
        this.emitter.setNamespace(namespace);
1✔
493

494
        this.appId = builder.appId;
1✔
495
        this.base64Encoded = builder.base64Encoded;
1✔
496

497
        this.subject = builder.subject;
1✔
498
        this.devicePlatform = builder.devicePlatform;
1✔
499
        this.sessionContext = builder.sessionContext;
1✔
500
        this.sessionCallbacks = builder.sessionCallbacks;
1✔
501
        this.threadCount = Math.max(builder.threadCount, 2);
1✔
502
        this.geoLocationContext = builder.geoLocationContext;
1✔
503
        this.mobileContext = builder.mobileContext;
1✔
504
        this.applicationCrash = builder.applicationCrash;
1✔
505
        this.trackerDiagnostic = builder.trackerDiagnostic;
1✔
506
        this.lifecycleEvents = builder.lifecycleEvents;
1✔
507
        this.activityTracking = builder.activityTracking;
1✔
508
        this.installTracking = builder.installTracking;
1✔
509
        this.applicationContext = builder.applicationContext;
1✔
510
        this.gdpr = builder.gdpr;
1✔
511
        this.level = builder.logLevel;
1✔
512
        this.trackerVersionSuffix = builder.trackerVersionSuffix;
1✔
513
        this.timeUnit = builder.timeUnit;
1✔
514
        this.foregroundTimeout = builder.foregroundTimeout;
1✔
515
        this.backgroundTimeout = builder.backgroundTimeout;
1✔
516
        this.userAnonymisation = builder.userAnonymisation;
1✔
517

518
        this.platformContext = new PlatformContext(this.context);
1✔
519

520
        setScreenContext(builder.screenContext);
1✔
521
        setDeepLinkContext(builder.deepLinkContext);
1✔
522

523
        if (trackerVersionSuffix != null) {
1✔
524
            String suffix = trackerVersionSuffix.replaceAll("[^A-Za-z0-9.-]", "");
1✔
525
            if (!suffix.isEmpty()) {
1✔
526
                trackerVersion = trackerVersion + " " + suffix;
1✔
527
            }
528
        }
529

530
        if (trackerDiagnostic && (level == LogLevel.OFF)) {
1✔
531
            level = LogLevel.ERROR;
×
532
        }
533

534
        Logger.updateLogLevel(level);
1✔
535

536
        // When session context is enabled
537
        if (this.sessionContext) {
1✔
538
            Runnable[] callbacks = {null, null, null, null};
1✔
539
            if (sessionCallbacks.length == 4) {
1✔
540
                callbacks = sessionCallbacks;
×
541
            }
542
            trackerSession = Session.getInstance(context, foregroundTimeout, backgroundTimeout, timeUnit, namespace, callbacks);
1✔
543
        }
544

545
        // Register notification receivers from singleton services
546
        registerNotificationHandlers();
1✔
547

548
        // Initialization of services shared with all the tracker instances
549
        initializeCrashReporting();
1✔
550
        initializeInstallTracking();
1✔
551
        initializeScreenviewTracking();
1✔
552
        initializeLifecycleTracking();
1✔
553

554
        // Resume session
555
        resumeSessionChecking();
1✔
556

557
        Logger.v(TAG, "Tracker created successfully.");
1✔
558
    }
1✔
559

560
    // --- Private init functions
561

562
    private void registerNotificationHandlers() {
563
        NotificationCenter.addObserver("SnowplowTrackerDiagnostic", receiveDiagnosticNotification);
1✔
564
        NotificationCenter.addObserver("SnowplowScreenView", receiveScreenViewNotification);
1✔
565
        NotificationCenter.addObserver("SnowplowLifecycleTracking", receiveLifecycleNotification);
1✔
566
        NotificationCenter.addObserver("SnowplowInstallTracking", receiveInstallNotification);
1✔
567
        NotificationCenter.addObserver("SnowplowCrashReporting", receiveCrashReportingNotification);
1✔
568
    }
1✔
569

570
    private void unregisterNotificationHandlers() {
571
        NotificationCenter.removeObserver(receiveDiagnosticNotification);
1✔
572
        NotificationCenter.removeObserver(receiveScreenViewNotification);
1✔
573
        NotificationCenter.removeObserver(receiveLifecycleNotification);
1✔
574
        NotificationCenter.removeObserver(receiveInstallNotification);
1✔
575
        NotificationCenter.removeObserver(receiveCrashReportingNotification);
1✔
576
    }
1✔
577

578
    private void initializeInstallTracking() {
579
        if (installTracking) {
1✔
580
            InstallTracker.getInstance(context);
1✔
581
        }
582
    }
1✔
583

584
    private void initializeScreenviewTracking() {
585
        if (activityTracking) {
1✔
586
            ActivityLifecycleHandler.getInstance(context);
1✔
587
        }
588
    }
1✔
589

590
    private void initializeLifecycleTracking() {
591
        if (lifecycleEvents) {
1✔
592
            ProcessObserver.initialize(context);
1✔
593
            // Initialize LifecycleStateMachine for lifecycle entities
594
            stateManager.addOrReplaceStateMachine(new LifecycleStateMachine(), "Lifecycle");
1✔
595
        }
596
    }
1✔
597

598
    private void initializeCrashReporting() {
599
        if (applicationCrash && !(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) {
1✔
600
            Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler());
1✔
601
        }
602
    }
1✔
603

604
    public void close() {
605
        unregisterNotificationHandlers();
1✔
606
        pauseSessionChecking();
1✔
607
        getEmitter().shutdown();
1✔
608
    }
1✔
609

610
    // --- Event Tracking Functions
611

612
    /**
613
     * Handles tracking the different types of events that
614
     * the Tracker can encounter.
615
     *
616
     * @param event the event to track
617
     * @return The event ID or null in case tracking is paused
618
     */
619
    public UUID track(final @NonNull Event event) {
620
        if (!dataCollection.get()) {
1✔
621
            return null;
1✔
622
        }
623
        event.beginProcessing(this);
1✔
624
        TrackerStateSnapshot stateSnapshot;
625
        TrackerEvent trackerEvent;
626
        synchronized (this) {
1✔
627
            stateSnapshot = stateManager.trackerStateForProcessedEvent(event);
1✔
628
            trackerEvent = new TrackerEvent(event, stateSnapshot);
1✔
629
            workaroundForIncoherentSessionContext(trackerEvent);
1✔
630
        }
1✔
631
        boolean reportsOnDiagnostic = !(event instanceof TrackerError);
1✔
632
        Executor.execute(reportsOnDiagnostic, TAG, () -> {
1✔
633
            transformEvent(trackerEvent);
1✔
634
            Payload payload = payloadWithEvent(trackerEvent);
1✔
635
            Logger.v(TAG, "Adding new payload to event storage: %s", payload);
1✔
636
            this.emitter.add(payload);
1✔
637
            event.endProcessing(this);
1✔
638
        });
1✔
639
        return trackerEvent.eventId;
1✔
640
    }
641

642
    private void transformEvent(@NonNull TrackerEvent event) {
643
        // Application_install event needs the timestamp to the real installation event.
644
        if (event.schema != null
1✔
645
                && event.schema.equals(TrackerConstants.SCHEMA_APPLICATION_INSTALL)
1✔
646
                && event.trueTimestamp != null)
647
        {
648
            event.timestamp = event.trueTimestamp;
1✔
649
            event.trueTimestamp = null;
1✔
650
        }
651
        // Payload can be optionally updated with values based on internal state
652
        stateManager.addPayloadValuesToEvent(event);
1✔
653
    }
1✔
654

655
    private @NonNull Payload payloadWithEvent(@NonNull TrackerEvent event) {
656
        TrackerPayload payload = new TrackerPayload();
1✔
657
        addBasicPropertiesToPayload(payload, event);
1✔
658
        if (event.isPrimitive) {
1✔
659
            addPrimitivePropertiesToPayload(payload, event);
1✔
660
        } else {
661
            addSelfDescribingPropertiesToPayload(payload, event);
1✔
662
        }
663
        List<SelfDescribingJson> contexts = event.contexts;
1✔
664
        addBasicContextsToContexts(contexts, event);
1✔
665
        addGlobalContextsToContexts(contexts, event);
1✔
666
        addStateMachineEntitiesToContexts(contexts, event);
1✔
667
        wrapContextsToPayload(payload, contexts);
1✔
668
        if (!event.isPrimitive) {
1✔
669
            // TODO: To remove when Atomic table refactoring is finished
670
            workaroundForCampaignAttributionEnrichment(payload, event, contexts);
1✔
671
        }
672
        return payload;
1✔
673
    }
674

675
    private void addBasicPropertiesToPayload(@NonNull Payload payload, @NonNull TrackerEvent event) {
676
        payload.add(Parameters.EID, event.eventId.toString());
1✔
677
        payload.add(Parameters.DEVICE_TIMESTAMP, Long.toString(event.timestamp));
1✔
678
        if (event.trueTimestamp != null) {
1✔
679
            payload.add(Parameters.TRUE_TIMESTAMP, event.trueTimestamp.toString());
×
680
        }
681
        payload.add(Parameters.APPID, this.appId);
1✔
682
        payload.add(Parameters.NAMESPACE, this.namespace);
1✔
683
        payload.add(Parameters.TRACKER_VERSION, this.trackerVersion);
1✔
684
        if (this.subject != null) {
1✔
685
            payload.addMap(new HashMap<>(this.subject.getSubject(userAnonymisation)));
1✔
686
        }
687
        payload.add(Parameters.PLATFORM, this.devicePlatform.getValue());
1✔
688
    }
1✔
689

690
    private void addPrimitivePropertiesToPayload(@NonNull Payload payload, @NonNull TrackerEvent event) {
691
        payload.add(Parameters.EVENT, event.eventName);
1✔
692
        payload.addMap(event.payload);
1✔
693
    }
1✔
694

695
    private void addSelfDescribingPropertiesToPayload(@NonNull Payload payload, @NonNull TrackerEvent event) {
696
        payload.add(Parameters.EVENT, TrackerConstants.EVENT_UNSTRUCTURED);
1✔
697

698
        SelfDescribingJson data = new SelfDescribingJson(event.schema, event.payload);
1✔
699
        HashMap<String, Object> unstructuredEventPayload = new HashMap<>();
1✔
700
        unstructuredEventPayload.put(Parameters.SCHEMA, TrackerConstants.SCHEMA_UNSTRUCT_EVENT);
1✔
701
        unstructuredEventPayload.put(Parameters.DATA, data.getMap());
1✔
702
        payload.addMap(unstructuredEventPayload, base64Encoded, Parameters.UNSTRUCTURED_ENCODED, Parameters.UNSTRUCTURED);
1✔
703
    }
1✔
704

705
    /*
706
     This is needed because the campaign-attribution-enrichment (in the pipeline) is able to parse
707
     the `url` and `referrer` only if they are part of a PageView event.
708
     The PageView event is an atomic event but the DeepLinkReceived and ScreenView are SelfDescribing events.
709
     For this reason we copy these two fields in the atomic fields in order to let the enrichment
710
     to process correctly the fields even if the event is not a PageView and it's a SelfDescribing event.
711
     This is a hack that should be removed once the atomic event table is dismissed and all the events
712
     will be SelfDescribing.
713
    */
714
    private void workaroundForCampaignAttributionEnrichment(@NonNull Payload payload, @NonNull TrackerEvent event, List<SelfDescribingJson> contexts) {
715
        String url = null;
1✔
716
        String referrer = null;
1✔
717

718
        if (event.schema.equals(DeepLinkReceived.SCHEMA) && event.payload != null) {
1✔
719
            url = (String)event.payload.get(DeepLinkReceived.PARAM_URL);
1✔
720
            referrer = (String)event.payload.get(DeepLinkReceived.PARAM_REFERRER);
1✔
721
        }
722
        else if (event.schema.equals(TrackerConstants.SCHEMA_SCREEN_VIEW) && contexts != null) {
1✔
723
            for (SelfDescribingJson entity : contexts) {
1✔
724
                if (entity instanceof DeepLink) {
1✔
725
                    DeepLink deepLink = (DeepLink) entity;
1✔
726
                    url = deepLink.getUrl();
1✔
727
                    referrer = deepLink.getReferrer();
1✔
728
                    break;
1✔
729
                }
730
            }
1✔
731
        }
732

733
        if (url != null) { payload.add(Parameters.PAGE_URL, url); }
1✔
734
        if (referrer != null) { payload.add(Parameters.PAGE_REFR, referrer); }
1✔
735
    }
1✔
736

737
    /*
738
     The session context should be computed as part of `addBasicContextsToContexts` but that method
739
     is executed in a separate thread. At the moment, the Session management is performed by the legacy
740
     solution that doesn't make use of the tracker state snapshot (taken as soon as the event is tracked)
741
     needed to keep a coherent state between events tracked in sequence but processed in parallel.
742
     Due to this parallel processing the session context could be incoherent with the event sequence.
743
     This limit is a serious problem when we process lifecycle events because the `application_background`
744
     event should be checked with the session foreground timeout and the `application_foreground` event
745
     should be checked with the session background timeout. Without a coherent session management it's
746
     impossible to grant this behaviour causing serious issues on the calculation of the session expiring.
747
     */
748
    private void workaroundForIncoherentSessionContext(@NonNull TrackerEvent event) {
749
        if (!event.isService && sessionContext) {
1✔
750
            String eventId = event.eventId.toString();
1✔
751
            long eventTimestamp = event.timestamp;
1✔
752
            Session sessionManager = trackerSession;
1✔
753
            if (sessionManager == null) {
1✔
754
                Logger.track(TAG, "Session not ready or method getHasLoadedFromFile returned false with eventId: %s", eventId);
×
755
                return;
×
756
            }
757
            SelfDescribingJson sessionContextJson = sessionManager.getSessionContext(eventId, eventTimestamp, userAnonymisation);
1✔
758
            event.contexts.add(sessionContextJson);
1✔
759
        }
760
    }
1✔
761

762
    private void addBasicContextsToContexts(@NonNull List<SelfDescribingJson> contexts, @NonNull TrackerEvent event) {
763
        if (applicationContext) {
1✔
764
            contexts.add(Util.getApplicationContext(this.context));
1✔
765
        }
766

767
        if (mobileContext) {
1✔
768
            contexts.add(platformContext.getMobileContext(userAnonymisation));
1✔
769
        }
770

771
        if (event.isService) {
1✔
772
            return;
×
773
        }
774

775
        if (geoLocationContext) {
1✔
776
            contexts.add(Util.getGeoLocationContext(this.context));
×
777
        }
778

779
        if (gdpr != null) {
1✔
780
            contexts.add(gdpr.getContext());
1✔
781
        }
782
    }
1✔
783

784
    private void addGlobalContextsToContexts(@NonNull List<SelfDescribingJson> contexts, @NonNull InspectableEvent event) {
785
        synchronized (globalContextGenerators) {
1✔
786
            for (GlobalContext generator : globalContextGenerators.values()) {
1✔
787
                contexts.addAll(generator.generateContexts(event));
1✔
788
            }
1✔
789
        }
1✔
790
    }
1✔
791

792
    private void addStateMachineEntitiesToContexts(@NonNull List<SelfDescribingJson> contexts, @NonNull InspectableEvent event) {
793
        List<SelfDescribingJson> stateManagerEntities = stateManager.entitiesForProcessedEvent(event);
1✔
794
        contexts.addAll(stateManagerEntities);
1✔
795
    }
1✔
796

797
    private void wrapContextsToPayload(@NonNull Payload payload, @NonNull List<SelfDescribingJson> contexts) {
798
        if (contexts.isEmpty()) {
1✔
799
            return;
1✔
800
        }
801
        List<Map<String, Object>> data = new LinkedList<>();
1✔
802
        for (SelfDescribingJson context : contexts) {
1✔
803
            if (context != null) {
1✔
804
                data.add(context.getMap());
1✔
805
            }
806
        }
1✔
807
        SelfDescribingJson finalContext = new SelfDescribingJson(TrackerConstants.SCHEMA_CONTEXTS, data);
1✔
808
        payload.addMap(finalContext.getMap(), base64Encoded, Parameters.CONTEXT_ENCODED, Parameters.CONTEXT);
1✔
809
    }
1✔
810

811
    // --- Helpers
812

813
    /**
814
     * Builds and adds a finalized payload of a service event
815
     * by adding in extra information to the payload:
816
     * - The event contexts (limited to identify the device and app)
817
     * - The Tracker Subject
818
     * - The Tracker parameters
819
     *
820
     * @param payload Payload the raw event payload to be
821
     *                decorated.
822
     * @param contexts The raw context list
823
     */
824
    private void addServiceEventPayload(@NonNull Payload payload, @NonNull List<SelfDescribingJson> contexts) {
825
        // Add default parameters to the payload
826
        payload.add(Parameters.PLATFORM, this.devicePlatform.getValue());
×
827
        payload.add(Parameters.APPID, this.appId);
×
828
        payload.add(Parameters.NAMESPACE, this.namespace);
×
829
        payload.add(Parameters.TRACKER_VERSION, this.trackerVersion);
×
830

831
        // If there is a subject present for the Tracker add it
832
        if (this.subject != null) {
×
833
            payload.addMap(new HashMap<>(this.subject.getSubject(userAnonymisation)));
×
834
        }
835

836
        // Add Mobile Context
837
        if (this.mobileContext) {
×
838
            contexts.add(platformContext.getMobileContext(userAnonymisation));
×
839
        }
840

841
        // Add application context
842
        if (this.applicationContext) {
×
843
            contexts.add(Util.getApplicationContext(this.context));
×
844
        }
845

846
        // If there are contexts to nest
847
        if (contexts.size() > 0) {
×
848
            List<Map> contextMaps = new LinkedList<>();
×
849
            for (SelfDescribingJson selfDescribingJson : contexts) {
×
850
                if (selfDescribingJson != null) {
×
851
                    contextMaps.add(selfDescribingJson.getMap());
×
852
                }
853
            }
×
854
            SelfDescribingJson envelope = new SelfDescribingJson(TrackerConstants.SCHEMA_CONTEXTS, contextMaps);
×
855
            payload.addMap(envelope.getMap(), this.base64Encoded, Parameters.CONTEXT_ENCODED,
×
856
                    Parameters.CONTEXT);
857
        }
858

859
        // Add this payload to the emitter
860
        this.emitter.add(payload);
×
861
    }
×
862

863
    // --- Controls
864

865
    /**
866
     * Starts event collection processes
867
     * again.
868
     */
869
    public void resumeEventTracking() {
870
        if (dataCollection.compareAndSet(false, true)) {
1✔
871
            resumeSessionChecking();
1✔
872
            getEmitter().flush();
1✔
873
        }
874
    }
1✔
875

876
    /**
877
     * Stops event collection and ends all
878
     * concurrent processes.
879
     */
880
    public void pauseEventTracking() {
881
        if (dataCollection.compareAndSet(true, false)) {
1✔
882
            pauseSessionChecking();
1✔
883
            getEmitter().shutdown();
1✔
884
        }
885
    }
1✔
886

887
    /**
888
     * Starts session checking.
889
     */
890
    public void resumeSessionChecking() {
891
        Session trackerSession = this.trackerSession;
1✔
892
        if (trackerSession != null) {
1✔
893
            trackerSession.setIsSuspended(false);
1✔
894
            Logger.d(TAG, "Session checking has been resumed.");
1✔
895
        }
896
    }
1✔
897

898
    /**
899
     * Ends session checking.
900
     */
901
    public void pauseSessionChecking() {
902
        Session trackerSession = this.trackerSession;
1✔
903
        if (trackerSession != null) {
1✔
904
            trackerSession.setIsSuspended(true);
1✔
905
            Logger.d(TAG, "Session checking has been paused.");
1✔
906
        }
907
    }
1✔
908

909
    /**
910
     * Convenience function for starting a new session.
911
     */
912
    public void startNewSession() {
913
        trackerSession.startNewSession();
1✔
914
    }
1✔
915

916
    // --- GDPR context
917

918
    /**
919
     * Enables GDPR context to be sent with every event.
920
     * @param basisForProcessing GDPR Basis for processing
921
     * @param documentId ID of a GDPR basis document
922
     * @param documentVersion Version of the document
923
     * @param documentDescription Description of the document
924
     */
925
    public void enableGdprContext(@NonNull Basis basisForProcessing, @Nullable String documentId, @Nullable String documentVersion, @Nullable String documentDescription) {
926
        this.gdpr = new Gdpr(basisForProcessing, documentId, documentVersion, documentDescription);
1✔
927
    }
1✔
928

929
    /**
930
     * Disable GDPR context.
931
     */
932
    public synchronized void disableGdprContext() {
933
        this.gdpr = null;
1✔
934
    }
1✔
935

936
    @Nullable
937
    public Gdpr getGdprContext() {
938
        return gdpr;
1✔
939
    }
940

941
    // --- Setters
942

943
    /**
944
     * @param subject a valid subject object
945
     */
946
    public void setSubject(@Nullable Subject subject) {
947
        this.subject = subject;
1✔
948
    }
1✔
949

950
    /**
951
     * @param emitter a valid emitter object
952
     */
953
    public void setEmitter(@NonNull Emitter emitter) {
954
        // Need to shutdown prior emitter before updating
955
        getEmitter().shutdown();
1✔
956

957
        // Set the new emitter
958
        this.emitter = emitter;
1✔
959
    }
1✔
960

961
    /**
962
     * Whether the session context should be sent with events
963
     * @param sessionContext
964
     */
965
    public synchronized void setSessionContext(boolean sessionContext) {
966
        this.sessionContext = sessionContext;
×
967
        if (trackerSession != null && !sessionContext) {
×
968
            pauseSessionChecking();
×
969
            trackerSession = null;
×
970
        } else if (trackerSession == null && sessionContext) {
×
971
            Runnable[] callbacks = {null, null, null, null};
×
972
            if (sessionCallbacks.length == 4) {
×
973
                callbacks = sessionCallbacks;
×
974
            }
975
            trackerSession = Session.getInstance(context, foregroundTimeout, backgroundTimeout, timeUnit, namespace, callbacks);
×
976
        }
977
    }
×
978

979
    /**
980
     * @param platform a valid DevicePlatform object
981
     */
982
    public void setPlatform(@NonNull DevicePlatform platform) {
983
        this.devicePlatform = platform;
1✔
984
    }
1✔
985

986
    /** Internal use only */
987
    public void setScreenContext(boolean screenContext) {
988
        this.screenContext = screenContext;
1✔
989
        if (screenContext) {
1✔
990
            stateManager.addOrReplaceStateMachine(new ScreenStateMachine(), "ScreenContext");
1✔
991
        } else {
992
            stateManager.removeStateMachine("ScreenContext");
1✔
993
        }
994
    }
1✔
995

996
    /** Internal use only */
997
    public void setDeepLinkContext(boolean deepLinkContext) {
998
        this.deepLinkContext = deepLinkContext;
1✔
999
        if (this.deepLinkContext) {
1✔
1000
            stateManager.addOrReplaceStateMachine(new DeepLinkStateMachine(), "DeepLinkContext");
1✔
1001
        } else {
1002
            stateManager.removeStateMachine("DeepLinkContext");
×
1003
        }
1004
    }
1✔
1005

1006
    /** Internal use only */
1007
    public void setUserAnonymisation(boolean userAnonymisation) {
1008
        if (this.userAnonymisation != userAnonymisation) {
1✔
1009
            this.userAnonymisation = userAnonymisation;
1✔
1010
            if (trackerSession != null) {
1✔
1011
                trackerSession.startNewSession();
1✔
1012
            }
1013
        }
1014
    }
1✔
1015

1016
    // --- Getters
1017

1018
    /** Internal use only */
1019
    public boolean getScreenContext() {
1020
        return screenContext;
×
1021
    }
1022

1023
    /** Internal use only */
1024
    public boolean getDeepLinkContext() {
1025
        return deepLinkContext;
×
1026
    }
1027

1028
    /** Internal use only */
1029
    public boolean getSessionContext() {
1030
        return sessionContext;
×
1031
    }
1032

1033
    /** Internal use only */
1034
    public boolean isUserAnonymisation() {
1035
        return userAnonymisation;
1✔
1036
    }
1037

1038
    /**
1039
     * @return the tracker version that was set
1040
     */
1041
    @NonNull
1042
    public String getTrackerVersion() {
1043
        return this.trackerVersion;
×
1044
    }
1045

1046
    /**
1047
     * @return the trackers subject object
1048
     */
1049
    @Nullable
1050
    public Subject getSubject() {
1051
        return this.subject;
1✔
1052
    }
1053

1054
    /**
1055
     * @return the emitter associated with the tracker
1056
     */
1057
    @NonNull
1058
    public Emitter getEmitter() {
1059
        return this.emitter;
1✔
1060
    }
1061

1062
    /**
1063
     * @return the trackers namespace
1064
     */
1065
    @NonNull
1066
    public String getNamespace() {
1067
        return this.namespace;
1✔
1068
    }
1069

1070
    /**
1071
     * @return the trackers set Application ID
1072
     */
1073
    @NonNull
1074
    public String getAppId() {
1075
        return this.appId;
1✔
1076
    }
1077

1078
    /**
1079
     * @return the base64 setting of the tracker
1080
     */
1081
    public boolean getBase64Encoded() {
1082
        return this.base64Encoded;
1✔
1083
    }
1084

1085
    /**
1086
     * @return the install tracking setting of the tracker
1087
     */
1088
    public boolean getInstallTracking() { return this.installTracking; }
1✔
1089

1090
    /**
1091
     * @return the application context setting of the tracker
1092
     */
1093
    public boolean getApplicationContext() {
1094
        return this.applicationContext;
1✔
1095
    }
1096

1097
    /**
1098
     * @return the trackers device platform
1099
     */
1100
    @NonNull
1101
    public DevicePlatform getPlatform() {
1102
        return this.devicePlatform;
1✔
1103
    }
1104

1105
    /**
1106
     * @return the trackers logging level
1107
     */
1108
    @NonNull
1109
    public LogLevel getLogLevel() {
1110
        return this.level;
1✔
1111
    }
1112

1113
    /**
1114
     * @return the trackers session object
1115
     */
1116
    @Nullable
1117
    public Session getSession() {
1118
        return this.trackerSession;
1✔
1119
    }
1120

1121
    /**
1122
     * @return the state of data collection
1123
     */
1124
    public boolean getDataCollection() {
1125
        return this.dataCollection.get();
1✔
1126
    }
1127

1128
    /**
1129
     * @return the amount of threads to use
1130
     */
1131
    public int getThreadCount() { return this.threadCount; }
1✔
1132

1133
    /**
1134
     * @return whether application crash tracking is on
1135
     */
1136
    public boolean getApplicationCrash() {
1137
        return this.applicationCrash;
1✔
1138
    }
1139

1140
    /**
1141
     * @return whether application lifecycle tracking is on
1142
     */
1143
    public boolean getLifecycleEvents() {
1144
        return this.lifecycleEvents;
1✔
1145
    }
1146

1147
    /**
1148
     * @return whether application lifecycle tracking is on
1149
     */
1150
    public boolean getActivityTracking() {
1151
        return this.activityTracking;
×
1152
    }
1153

1154
    /**
1155
     * Internal use only
1156
     * @return screen state from tracker
1157
     */
1158
    @Nullable
1159
    public ScreenState getScreenState() {
1160
        State state = stateManager.trackerState.getState("ScreenContext");
1✔
1161
        if (state == null) {
1✔
1162
            // Legacy initialization
1163
            return new ScreenState();
1✔
1164
        };
1165
        if (state instanceof ScreenState) {
1✔
1166
            return (ScreenState) state;
1✔
1167
        }
1168
        return null;
×
1169
    }
1170

1171
    // --- Global contexts
1172

1173
    public void setGlobalContextGenerators(@NonNull Map<String, GlobalContext> globalContexts) {
1174
        Objects.requireNonNull(globalContexts);
1✔
1175
        synchronized (globalContextGenerators) {
1✔
1176
            globalContextGenerators.clear();
1✔
1177
            globalContextGenerators.putAll(globalContexts);
1✔
1178
        }
1✔
1179
    }
1✔
1180

1181
    public boolean addGlobalContext(@NonNull GlobalContext generator, @NonNull String tag) {
1182
        Objects.requireNonNull(generator);
1✔
1183
        Objects.requireNonNull(tag);
1✔
1184
        synchronized (globalContextGenerators) {
1✔
1185
            if (globalContextGenerators.containsKey(tag)) {
1✔
1186
                return false;
1✔
1187
            }
1188
            globalContextGenerators.put(tag, generator);
1✔
1189
            return true;
1✔
1190
        }
×
1191
    }
1192

1193
    @Nullable
1194
    public GlobalContext removeGlobalContext(@NonNull String tag) {
1195
        Objects.requireNonNull(tag);
1✔
1196
        synchronized (globalContextGenerators) {
1✔
1197
            return globalContextGenerators.remove(tag);
1✔
1198
        }
×
1199
    }
1200

1201
    @NonNull
1202
    public Set<String> getGlobalContextTags() {
1203
        return globalContextGenerators.keySet();
1✔
1204
    }
1205
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc