Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
cache: gradle
- name: Set up Android SDK
uses: android-actions/setup-android@v3
with:
packages: ''
- name: Install Android SDK packages
run: sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0"
- name: Lint
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Change Log

## [1.4.0] - 2026-10-21

### Added
- `Sift.setScreenTitle(String)` sets the screen name reported with App State events, overriding the automatically collected Activity class name. Passing `null` or an empty string resets it to the automatic name. Mirrors the iOS SDK's `setScreenTitle:`, so cross-platform wrappers can set and clear a screen name the same way on both platforms.

## [1.3.1] - 2026-03-03

### Breaking Changes
Expand Down
9 changes: 8 additions & 1 deletion DESIGNDOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Class diagram of AndroidDeviceLocation
The AndroidAppState collects the following informations:

- **activity\_class\_name** : {type: string, required: false}
- The activity class name indicates the current activity/fragment class name from where the data are collected.
- The activity class name indicates the current activity/fragment class name from where the data are collected; overridden by the screen name set through `setScreenTitle`.
- **location** : {type: AndroidDeviceLocation, required: false}
- The location consists of collective information of latitude, longitude, accuracy and the time at which data was collected as shown in the [section 3.1](#31-androidDeviceLocation). (_Have data only if the sift configuration and permissions are enabled_)
- **sdk\_version** : {type: string, required: false}
Expand Down Expand Up @@ -328,6 +328,11 @@ Following are the static API to interact with SDK:
- It will set the provided id as the userId inside the sift instance.
- **unsetUserId**()
- Which removes any presetted useId to null
- **setScreenTitle**(_title_)
- It will set a screen name that is attached to every later App State collection, until it is changed or reset.
- The name overrides the Activity class name collected automatically. Passing null or an empty string resets it, after which the automatic name is used again.
- It does not collect by itself; call Sift.collect() to send an event right away.
- The title is not persisted, so it does not survive a process restart.

### 4.2 SIFT IMPL

Expand Down Expand Up @@ -499,6 +504,8 @@ It provides the following instance methods:

- **setActivityName**(_activityName_)
- Which will assign the current activity class name as the provided name.
- **setManualScreenTitle**(_title_)
- Which records the screen name used instead of the automatic activity class name in all later collections. A null or empty title resets it.
- **collect**()
- This will request location updates if Sift.Config _disallowLocationCollection_ is false and location permission is enabled by the user which was requested by your application (_NB: Sift will not request permissions that are not granted by the user from your application_). Otherwise it will collect the rest of app state events without location information.
- The location collection is managed by FusedLocationProviderClient. The location request is configured as PRIORITY\_HIGH\_ACCURACY with one minute interval and 10 seconds fastest interval.
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,21 @@ If the user logs out of your application, you should unset the user id:
Sift.unsetUserId();
```

#### Set a custom screen name

Sift reports the Activity class name with App State events. If your application
does not map screens to Activities, set the screen name yourself:

```java
Sift.setScreenTitle("Checkout");
```

Pass `null` or an empty string to clear it and go back to the automatic name:

```java
Sift.setScreenTitle(null);
```

<a name="custom"></a>
### Custom Integration
#### Initialize Sift in your main Activity
Expand Down Expand Up @@ -209,6 +224,21 @@ If the user logs out of your application, you should unset the user id:
Sift.unsetUserId();
```

#### Set a custom screen name

Sift reports the Activity class name with App State events. If your application
does not map screens to Activities, set the screen name yourself:

```java
Sift.setScreenTitle("Checkout");
```

Pass `null` or an empty string to clear it and go back to the automatic name:

```java
Sift.setScreenTitle(null);
```

<a name="submitting_an_issue"></a>
## Submitting an issue

Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true

VERSION_NAME=1.3.1
VERSION_CODE=26
VERSION_NAME=1.4.0
VERSION_CODE=27

GROUP=com.siftscience

Expand Down
19 changes: 18 additions & 1 deletion sift/src/main/java/siftscience/android/AppStateCollector.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public class AppStateCollector {
private final Context context;

private String activityClassName;
private volatile String manualScreenTitle;
private boolean acquiredNewLocation;
private LocationRequest locationRequest;
private FusedLocationProviderClient mFusedLocationClient;
Expand Down Expand Up @@ -83,6 +84,22 @@ public void setActivityName(String activityName) {
this.activityClassName = activityName;
}

/**
* Record the screen name used instead of the automatic Activity class name
* in all later collections.
*
* A null or empty title resets it; the automatic name is used again.
*/
public void setManualScreenTitle(String title) {
this.manualScreenTitle = (title == null || title.isEmpty()) ? null : title;
}

/** @return the manual screen title when one is set, the automatic name otherwise. */
private String screenTitle() {
String manual = this.manualScreenTitle;
return manual != null ? manual : this.activityClassName;
}

public void collect() {
if (!sift.getConfig().disallowLocationCollection &&
this.mFusedLocationClient != null &&
Expand Down Expand Up @@ -200,7 +217,7 @@ private AndroidAppStateJson get() {
List<String> ipAddresses = getIpAddresses();

AndroidAppStateJson androidAppStateJson = new AndroidAppStateJson()
.withActivityClassName(this.activityClassName)
.withActivityClassName(this.screenTitle())
.withBatteryLevel(batteryLevel)
.withBatteryState((long) status)
.withBatteryHealth((long) health)
Expand Down
27 changes: 27 additions & 0 deletions sift/src/main/java/siftscience/android/Sift.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public final class Sift {
private static volatile AppStateCollector appStateCollector;
private static volatile String unboundUserId;
private static volatile boolean hasUnboundUserId = false;
private static volatile String unboundScreenTitle;
private static volatile boolean hasUnboundScreenTitle = false;
private static volatile ExecutorService executors;

//================================================================================
Expand Down Expand Up @@ -81,6 +83,11 @@ public static void open(@NonNull Context context,
appStateCollector = new AppStateCollector(instance, c);
unboundUserId = null;
hasUnboundUserId = false;
if (hasUnboundScreenTitle) {
appStateCollector.setManualScreenTitle(unboundScreenTitle);
unboundScreenTitle = null;
hasUnboundScreenTitle = false;
}
} else {
if (config != null) {
instance.setConfig(config);
Expand Down Expand Up @@ -200,6 +207,26 @@ public static synchronized void unsetUserId() {
setUserId(null);
}

/**
* Set the screen name attached to every later App State collection, until it
* is changed or reset.
*
* Overrides the Activity class name collected automatically. Pass null or an
* empty string to reset, after which the automatic name is used again. It
* does not collect by itself; call Sift.collect() to send an event now.
*
* The title is not persisted, so it does not survive a process restart.
*/
public static synchronized void setScreenTitle(String title) {
AppStateCollector localAppStateCollector = appStateCollector;
if (localAppStateCollector != null) {
localAppStateCollector.setManualScreenTitle(title);
} else {
unboundScreenTitle = title;
hasUnboundScreenTitle = true;
}
}

//================================================================================
// Instance config
//================================================================================
Expand Down
82 changes: 82 additions & 0 deletions sift/src/test/java/siftscience/android/SiftTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,88 @@ public void testAppStateCollector() {
assertEquals(Sift.SDK_VERSION, event.getAndroidAppState().getSdkVersion());
}

@Test
public void testManualScreenTitleOverridesActivityName() {
MemorySharedPreferences preferences = new MemorySharedPreferences();

Sift.Config config = new Sift.Config.Builder().withDisallowLocationCollection(true).build();

SiftImpl sift = new SiftImpl(mockContext(preferences), config, "",
false, mockTaskManager());

AppStateCollector appStateCollector = new AppStateCollector(sift, mockContext(preferences));
appStateCollector.setActivityName("MainActivity");
appStateCollector.setManualScreenTitle("Checkout");

appStateCollector.collect();
// Collect twice because the first one gets uploaded and flushed
appStateCollector.collect();

List<MobileEventJson> eventList = sift.getQueue(SiftImpl.APP_STATE_QUEUE_IDENTIFIER).flush();
assertEquals(1, eventList.size());
assertEquals("Checkout",
eventList.get(0).getAndroidAppState().getActivityClassName());
}

@Test
public void testManualScreenTitleResetFallsBackToActivityName() {
MemorySharedPreferences preferences = new MemorySharedPreferences();

Sift.Config config = new Sift.Config.Builder().withDisallowLocationCollection(true).build();

SiftImpl sift = new SiftImpl(mockContext(preferences), config, "",
false, mockTaskManager());

AppStateCollector appStateCollector = new AppStateCollector(sift, mockContext(preferences));
appStateCollector.setActivityName("MainActivity");
appStateCollector.setManualScreenTitle("Checkout");

appStateCollector.collect();
// Collect twice because the first one gets uploaded and flushed
appStateCollector.collect();
assertEquals("Checkout", sift.getQueue(SiftImpl.APP_STATE_QUEUE_IDENTIFIER)
.flush().get(0).getAndroidAppState().getActivityClassName());

appStateCollector.setManualScreenTitle(null);
appStateCollector.collect();
assertEquals("MainActivity", sift.getQueue(SiftImpl.APP_STATE_QUEUE_IDENTIFIER)
.flush().get(0).getAndroidAppState().getActivityClassName());

appStateCollector.setManualScreenTitle("Checkout");
appStateCollector.setManualScreenTitle("");
appStateCollector.collect();
assertEquals("MainActivity", sift.getQueue(SiftImpl.APP_STATE_QUEUE_IDENTIFIER)
.flush().get(0).getAndroidAppState().getActivityClassName());
}

@Test
public void testUnboundSetScreenTitle() throws NoSuchFieldException, IllegalAccessException {
MemorySharedPreferences preferences = new MemorySharedPreferences();

resetSiftStatics();

Sift.setScreenTitle("Checkout");
Sift.open(mockContext(preferences),
new Sift.Config.Builder().withDisallowLocationCollection(true).build());

Field collectorField = Sift.class.getDeclaredField("appStateCollector");
collectorField.setAccessible(true);
AppStateCollector appStateCollector = (AppStateCollector) collectorField.get(Sift.class);

Field titleField = AppStateCollector.class.getDeclaredField("manualScreenTitle");
titleField.setAccessible(true);
assertEquals("Checkout", titleField.get(appStateCollector));
}

private void resetSiftStatics() throws NoSuchFieldException, IllegalAccessException {
for (String name : Arrays.asList("instance", "appStateCollector",
"devicePropertiesCollector")) {
Field field = Sift.class.getDeclaredField(name);
field.setAccessible(true);
field.set(Sift.class, null);
}
}

@Test
public void testDevicePropertyCollector() throws InterruptedException {
MemorySharedPreferences preferences = new MemorySharedPreferences();
Expand Down
Loading