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

snowplow / snowplow-android-tracker / #1052

pending completion
#1052

push

github-actions

web-flow
Fix crash with null pointer when accessing session (close #581)

PR #582

9 of 9 new or added lines in 3 files covered. (100.0%)

3497 of 4579 relevant lines covered (76.37%)

0.76 hits per line

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

86.73
/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");
1✔
471
                if (event != null) {
1✔
472
                    track(event);
1✔
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
            if (sessionContextJson != null) {
1✔
759
                event.contexts.add(sessionContextJson);
1✔
760
            }
761
        }
762
    }
1✔
763

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

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

773
        if (event.isService) {
1✔
774
            return;
×
775
        }
776

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

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

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

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

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

813
    // --- Helpers
814

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

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

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

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

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

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

865
    // --- Controls
866

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

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

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

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

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

918
    // --- GDPR context
919

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

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

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

943
    // --- Setters
944

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

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

959
        // Set the new emitter
960
        this.emitter = emitter;
1✔
961
    }
1✔
962

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

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

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

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

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

1018
    // --- Getters
1019

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1173
    // --- Global contexts
1174

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

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

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

1203
    @NonNull
1204
    public Set<String> getGlobalContextTags() {
1205
        return globalContextGenerators.keySet();
1✔
1206
    }
1207
}
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