Skip to content

Commit

Permalink
feat: added new way for tracking life cycle events in android using l…
Browse files Browse the repository at this point in the history
…ife cycle observer (#225)

* fix: using AtomicIntegers in Application life cycle manager

* feat: added support for new way of tracking life cycle events and deep links

* chore: minor refactors based on review comments

* chore: upgraded kotlin and gradle versions in all modules

* chore: reading writekey and dataplane url from local.properties

* feat: made dependencies for tracking new life cycle events compile only and needed to be added by customer based on requirement

* chore: minor changes

* fix: code structure (#233)

* fix: setting isFirstLaunch to false on firstLaunch even when user opted out

* chore: using a primitive boolean expression

* fix: fixed merge issue in sample-kotlin gradle file

* chore: bumped the version of androix.core:core-ktx to 1.10.1

---------

Co-authored-by: Desu Sai Venkat <venkat@rudderstack.com>
Co-authored-by: Debanjan Chatterjee <debanjanchatterjee99@gmail.com>
  • Loading branch information
3 people authored Jun 12, 2023
1 parent a0212f0 commit ae5a938
Show file tree
Hide file tree
Showing 16 changed files with 455 additions and 254 deletions.
7 changes: 6 additions & 1 deletion core/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apply plugin: 'com.android.library'

android {
compileSdkVersion 31
compileSdkVersion 33

defaultConfig {
minSdkVersion 19
Expand Down Expand Up @@ -40,12 +40,17 @@ dependencies {
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'androidx.annotation:annotation:1.1.0'

// required for new life cycle methods
compileOnly 'androidx.lifecycle:lifecycle-process:2.6.1'
compileOnly 'androidx.lifecycle:lifecycle-common:2.6.1'

testImplementation ('com.android.support.test:rules:1.0.2')
testImplementation 'com.android.support.test:runner:1.0.2'
testImplementation 'org.robolectric:robolectric:4.3'
testImplementation 'androidx.test:core-ktx:1.5.0'
testImplementation "org.hamcrest:hamcrest:2.2"
testImplementation "org.mockito:mockito-core:3.11.2"
testImplementation 'androidx.lifecycle:lifecycle-process:2.6.1'

testImplementation "org.powermock:powermock-core:2.0.9"
testImplementation ("org.powermock:powermock-module-junit4:2.0.9"){
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.rudderstack.android.sdk.core;

import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;

public class AndroidXLifeCycleManager implements DefaultLifecycleObserver {
private ApplicationLifeCycleManager applicationLifeCycleManager;
private RudderUserSessionManager userSessionManager;

public AndroidXLifeCycleManager(ApplicationLifeCycleManager applicationLifeCycleManager, RudderUserSessionManager userSessionManager) {
this.applicationLifeCycleManager = applicationLifeCycleManager;
this.userSessionManager = userSessionManager;
}

@Override
public void onStart(@NonNull LifecycleOwner owner) {
userSessionManager.startSessionTrackingIfApplicable();
applicationLifeCycleManager.sendApplicationOpened();
}

@Override
public void onStop(@NonNull LifecycleOwner owner) {
applicationLifeCycleManager.sendApplicationBackgrounded();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,74 +5,40 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.rudderstack.android.sdk.core.util.Utils;

import java.util.concurrent.atomic.AtomicBoolean;

public class ApplicationLifeCycleManager implements Application.ActivityLifecycleCallbacks {

private int noOfActivities;
private final AtomicBoolean isFirstLaunch = new AtomicBoolean(true);
private final RudderPreferenceManager preferenceManager;
private final EventRepository repository;
private final RudderFlushWorkManager rudderFlushWorkManager;
public class ApplicationLifeCycleManager {
private final RudderConfig config;
private RudderUserSession userSession;

private final Application application;
private final RudderFlushWorkManager rudderFlushWorkManager;
private final EventRepository repository;
public static final String VERSION = "version";
private static final AtomicBoolean isFirstLaunch = new AtomicBoolean(true);
private final RudderPreferenceManager preferenceManager;

ApplicationLifeCycleManager(RudderPreferenceManager preferenceManager, EventRepository repository, RudderFlushWorkManager rudderFlushWorkManager, RudderConfig config) {
this.preferenceManager = preferenceManager;
this.repository = repository;
this.rudderFlushWorkManager = rudderFlushWorkManager;
public ApplicationLifeCycleManager(RudderConfig config, Application application,
RudderFlushWorkManager rudderFlushWorkManager,
EventRepository repository,
RudderPreferenceManager preferenceManager) {
this.config = config;
}

public void start(Application application) {
startSessionTracking();
this.sendApplicationUpdateStatus(application);
if (config.isTrackLifecycleEvents() || config.isRecordScreenViews()) {
application.registerActivityLifecycleCallbacks(this);
}
}

private void startSessionTracking() {
RudderLogger.logDebug("ApplicationLifecycleManager: startSessionTracking: Initiating RudderUserSession");
userSession = new RudderUserSession(preferenceManager, config);

// 8. clear session if automatic session tracking was enabled previously
// but disabled presently or vice versa.
boolean previousAutoSessionTrackingStatus = preferenceManager.getAutoSessionTrackingStatus();
if (previousAutoSessionTrackingStatus != config.isTrackAutoSession()) {
userSession.clearSession();
}
preferenceManager.saveAutoSessionTrackingStatus(config.isTrackAutoSession());
// starting automatic session tracking if enabled.
if (config.isTrackLifecycleEvents() && config.isTrackAutoSession()) {
userSession.startSessionIfNeeded();
}
}

void applySessionTracking(RudderMessage message) {
// Session Tracking
if (userSession.getSessionId() != null) {
message.setSession(userSession);
}
if (config.isTrackLifecycleEvents() && config.isTrackAutoSession()) {
userSession.updateLastEventTimeStamp();
}
this.application = application;
this.rudderFlushWorkManager = rudderFlushWorkManager;
this.repository = repository;
this.preferenceManager = preferenceManager;
}

/*
* Check if App is installed for the first time or it is updated.
* If it is the first time then make LifeCycle event: Application Installed.
* If it is updated then make LifeCycle event: Application Updated.
*/
private void sendApplicationUpdateStatus(Application application) {
void trackApplicationUpdateStatus() {
if (!this.config.isTrackLifecycleEvents() && !this.config.isNewLifeCycleEvents()) {
return;
}
AppVersion appVersion = new AppVersion(application);
if (appVersion.previousBuild == -1) {
// application was not installed previously, now triggering Application Installed event
Expand All @@ -83,149 +49,81 @@ private void sendApplicationUpdateStatus(Application application) {
appVersion.storeCurrentBuildAndVersion();
sendApplicationUpdated(appVersion.previousBuild, appVersion.currentBuild, appVersion.previousVersion, appVersion.currentVersion);
}

}

private void sendApplicationInstalled(int currentBuild, String currentVersion) {
// If trackLifeCycleEvents is not allowed then discard the event
if (!config.isTrackLifecycleEvents()) {
return;
}
void sendApplicationInstalled(int currentBuild, String currentVersion) {
RudderLogger.logDebug("ApplicationLifeCycleManager: sendApplicationInstalled: Tracking Application Installed");
RudderMessage message = new RudderMessageBuilder()
.setEventName("Application Installed")
.setProperty(
new RudderProperty()
.putValue("version", currentVersion)
.putValue(VERSION, currentVersion)
.putValue("build", currentBuild)
).build();
message.setType(MessageType.TRACK);
repository.processMessage(message);
}

private void sendApplicationUpdated(int previousBuild, int currentBuild, String previousVersion, String currentVersion) {
// If either optOut() is set to true or LifeCycleEvents set to false then discard the event
if (repository.getOptStatus() || !config.isTrackLifecycleEvents()) {
void sendApplicationUpdated(int previousBuild, int currentBuild, String previousVersion, String currentVersion) {
if (repository.getOptStatus()) {
return;
}
// Application Updated event
RudderLogger.logDebug("ApplicationLifeCycleManager: sendApplicationInstalled: Tracking Application Updated");
RudderLogger.logDebug("ApplicationLifeCycleManager: sendApplicationUpdated: Tracking Application Updated");
RudderMessage message = new RudderMessageBuilder().setEventName("Application Updated")
.setProperty(
new RudderProperty()
.putValue("previous_version", previousVersion)
.putValue("version", currentVersion)
.putValue(VERSION, currentVersion)
.putValue("previous_build", previousBuild)
.putValue("build", currentBuild)
).build();
message.setType(MessageType.TRACK);
repository.processMessage(message);
}

@Override
public void onActivityStarted(@NonNull Activity activity) {
if (this.config.isTrackLifecycleEvents()) {
noOfActivities += 1;
if (noOfActivities == 1) {
// If user has disabled tracking activities (i.e., set optOut() to true)
// then discard the event
if (repository.getOptStatus()) {
return;
}
startSessionTrackingIfApplicable();
RudderMessage trackMessage;
trackMessage = new RudderMessageBuilder()
.setEventName("Application Opened")
.setProperty(Utils.trackDeepLink(activity, isFirstLaunch, preferenceManager.getVersionName()))
.build();
trackMessage.setType(MessageType.TRACK);
repository.processMessage(trackMessage);
}
}
if (config.isRecordScreenViews()) {
// If user has disabled tracking activities (i.e., set optOut() to true)
// then discard the event
if (repository.getOptStatus()) {
return;
}
ScreenPropertyBuilder screenPropertyBuilder = new ScreenPropertyBuilder().setScreenName(activity.getLocalClassName()).isAtomatic(true);
RudderMessage screenMessage = new RudderMessageBuilder().setEventName(activity.getLocalClassName()).setProperty(screenPropertyBuilder.build()).build();
screenMessage.setType(MessageType.SCREEN);
repository.processMessage(screenMessage);
}
}

private void startSessionTrackingIfApplicable() {
// Session Tracking
// Automatic tracking session started
if (!isFirstLaunch.get() && config.isTrackAutoSession() && userSession != null) {
userSession.startSessionIfNeeded();
void sendApplicationOpened() {
Boolean isFirstLaunchValue = isFirstLaunch.getAndSet(false);
if (repository.getOptStatus()) {
return;
}
}

@Override
public void onActivityStopped(@NonNull Activity activity) {
if (this.config.isTrackLifecycleEvents()) {
noOfActivities -= 1;
if (noOfActivities == 0) {
// If user has disabled tracking activities (i.e., set optOut() to true)
// then discard the event
if (repository.getOptStatus()) {
return;
}
RudderMessage message = new RudderMessageBuilder().setEventName("Application Backgrounded").build();
message.setType(MessageType.TRACK);
repository.processMessage(message);
}
RudderProperty rudderProperty = new RudderProperty().putValue("from_background", !isFirstLaunch.get());
if (Boolean.TRUE.equals(isFirstLaunchValue)) {
rudderProperty.putValue(VERSION, preferenceManager.getVersionName());
}
RudderMessage trackMessage = new RudderMessageBuilder()
.setEventName("Application Opened")
.setProperty(rudderProperty)
.build();
trackMessage.setType(MessageType.TRACK);
repository.processMessage(trackMessage);
}

void startSession(Long sessionId) {
if (config.isTrackAutoSession()) {
endSession();
config.setTrackAutoSession(false);
void sendApplicationBackgrounded() {
if (repository.getOptStatus()) {
return;
}
userSession.startSession(sessionId);
RudderMessage message = new RudderMessageBuilder().setEventName("Application Backgrounded").build();
message.setType(MessageType.TRACK);
repository.processMessage(message);
}

void endSession() {
if (config.isTrackAutoSession()) {
config.setTrackAutoSession(false);
void recordScreenView(@NonNull Activity activity) {
if (repository.getOptStatus()) {
return;
}
userSession.clearSession();
}

@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) {
// Empty
}

@Override
public void onActivityResumed(@NonNull Activity activity) {
// Empty
}

@Override
public void onActivityPaused(@NonNull Activity activity) {
// Empty
ScreenPropertyBuilder screenPropertyBuilder = new ScreenPropertyBuilder().setScreenName(activity.getLocalClassName()).isAutomatic(true);
RudderMessage screenMessage = new RudderMessageBuilder().setEventName(activity.getLocalClassName()).setProperty(screenPropertyBuilder.build()).build();
screenMessage.setType(MessageType.SCREEN);
repository.processMessage(screenMessage);
}

@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle bundle) {
// Empty
}

@Override
public void onActivityDestroyed(@NonNull Activity activity) {
// Empty
}

public void reset() {
if (userSession.getSessionId() != null) {
userSession.refreshSession();
}
public static Boolean isFirstLaunch() {
return isFirstLaunch.get();
}

private class AppVersion {

int previousBuild;
int currentBuild;
String previousVersion;
Expand Down Expand Up @@ -264,6 +162,7 @@ void storeCurrentBuildAndVersion() {
preferenceManager.saveVersionName(currentVersion);
}
}

}
/*create or replace view app_opened_installed as select result.event_name, result.userId,
result.timestamp as sent_at, result.platform as platform, result.writeKey as write_key, result.anon_id as anonymous_id, result.sdk_version as sdk_version from (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class Constants {
static final boolean AUTO_COLLECT_ADVERT_ID = false;
// whether we should trackLifecycle events
static final boolean TRACK_LIFECYCLE_EVENTS = true;
// whether we should use the new way of tracking life cycle events
static final boolean NEW_LIFECYCLE_EVENTS = false;
// whether we should track the deep link events or not
static final boolean TRACK_DEEP_LINKS = true;
// whether we should record screen views automatically
static final boolean RECORD_SCREEN_VIEWS = false;
// minimum duration for inactivity is 0 milliseconds
Expand Down
Loading

0 comments on commit ae5a938

Please sign in to comment.