Skip to content
Merged
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: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]

### Fixed
- Push token registration and disable operations, including their API requests and retries, now run on a dedicated SDK-owned serial executor instead of Android's process-wide `AsyncTask` queue. This prevents host app background work from delaying device registration, preserves operation order, and keeps client callbacks on the main thread.
- Push token registration and disable operations, including their API requests and retries, now run on a dedicated SDK-owned serial executor, while Iterable deep-link redirects use a separate SDK-owned serial executor. This removes their dependency on Android's process-wide `AsyncTask` queue, preserves ordering within each operation type, prevents slow redirects from delaying push work, and keeps client callbacks and attribution updates on the main thread.

## [3.11.0]
### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1420,7 +1420,7 @@ private boolean checkIfMessageIsNull(@Nullable IterableInAppMessage message, @Nu
* or the original url if it is not an Iterable link.
*/
public void getAndTrackDeepLink(@NonNull String uri, @NonNull IterableHelper.IterableActionHandler onCallback) {
IterableDeeplinkManager.getAndTrackDeeplink(uri, onCallback);
IterableDeeplinkManager.getAndTrackDeepLink(uri, onCallback);
}

/**
Expand Down Expand Up @@ -1452,7 +1452,7 @@ public boolean handleAppLink(@NonNull String uri) {
IterableLogger.printInfo();

if (IterableDeeplinkManager.isIterableDeeplink(uri)) {
IterableDeeplinkManager.getAndTrackDeeplink(uri, new IterableHelper.IterableActionHandler() {
IterableDeeplinkManager.getAndTrackDeepLink(uri, new IterableHelper.IterableActionHandler() {
@Override
public void execute(String originalUrl) {
IterableAction action = IterableAction.actionOpenUrl(originalUrl);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,44 @@
package com.iterable.iterableapi;

import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

class IterableDeeplinkManager {

private static Pattern deeplinkPattern = Pattern.compile(IterableConstants.ITBL_DEEPLINK_IDENTIFIER);
private static final Pattern DEEP_LINK_PATTERN =
Pattern.compile(IterableConstants.ITBL_DEEPLINK_IDENTIFIER);

/**
* Tracks a link click and passes the redirected URL to the callback
* @param url The URL that was clicked
* @param callback The callback to execute the original URL is retrieved
*/
static void getAndTrackDeeplink(@Nullable String url, @NonNull IterableHelper.IterableActionHandler callback) {
if (url != null) {
if (!IterableUtil.isUrlOpenAllowed(url)) {
return;
}
if (isIterableDeeplink(url)) {
new RedirectTask(callback).execute(url);
} else {
callback.execute(url);
}
} else {
static void getAndTrackDeepLink(@Nullable String url, @NonNull IterableHelper.IterableActionHandler callback) {
if (url == null) {
callback.execute(null);
return;
}

if (!IterableUtil.isUrlOpenAllowed(url)) {
return;
}

if (!isIterableDeeplink(url)) {
callback.execute(url);
return;
}

IterableDeeplinkRedirectTask redirectTask =
new IterableDeeplinkRedirectTask(
url,
callback,
new IterableDeeplinkRedirectResolver(),
IterableExecutors.main()
);
IterableExecutors.deepLink().execute(redirectTask);
}

/**
Expand All @@ -43,93 +48,11 @@ static void getAndTrackDeeplink(@Nullable String url, @NonNull IterableHelper.It
*/
static boolean isIterableDeeplink(@Nullable String url) {
if (url != null) {
Matcher m = deeplinkPattern.matcher(url);
Matcher m = DEEP_LINK_PATTERN.matcher(url);
if (m.find()) {
return true;
}
}
return false;
}

private static class RedirectTask extends AsyncTask<String, Void, String> {
static final String TAG = "RedirectTask";
static final int DEFAULT_TIMEOUT_MS = 3000; //3 seconds

private IterableHelper.IterableActionHandler callback;

public int campaignId;
public int templateId;
public String messageId;

RedirectTask(IterableHelper.IterableActionHandler callback) {
this.callback = callback;
}

@Override
protected String doInBackground(String... params) {
if (params == null || params.length == 0) {
return null;
}

String urlString = params[0];
HttpURLConnection urlConnection = null;

try {
URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setReadTimeout(DEFAULT_TIMEOUT_MS);
urlConnection.setInstanceFollowRedirects(false);

int responseCode = urlConnection.getResponseCode();

if (responseCode >= 400) {
IterableLogger.d(TAG, "Invalid Request for: " + urlString + ", returned code " + responseCode);
} else if (responseCode >= 300) {
urlString = urlConnection.getHeaderField(IterableConstants.LOCATION_HEADER_FIELD);
try {
List<String> cookieHeaders = urlConnection.getHeaderFields().get("Set-Cookie");
if (cookieHeaders != null) {
ArrayList<HttpCookie> httpCookies = new ArrayList<>(cookieHeaders.size());
for (String cookieString : cookieHeaders) {
List<HttpCookie> cookies = HttpCookie.parse(cookieString);
if (cookies != null) {
httpCookies.addAll(cookies);
}
}
for (HttpCookie cookie : httpCookies) {
if (cookie.getName().equals("iterableEmailCampaignId")) {
campaignId = Integer.parseInt(cookie.getValue());
} else if (cookie.getName().equals("iterableTemplateId")) {
templateId = Integer.parseInt(cookie.getValue());
} else if (cookie.getName().equals("iterableMessageId")) {
messageId = cookie.getValue();
}
}
}
} catch (Exception e) {
IterableLogger.e(TAG, "Error while parsing cookies: " + e.getMessage());
}
}
} catch (Exception e) {
IterableLogger.e(TAG, e.getMessage());
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
}
return urlString;
}

@Override
protected void onPostExecute(String s) {
if (callback != null) {
callback.execute(s);
}

if (campaignId != 0) {
IterableAttributionInfo attributionInfo = new IterableAttributionInfo(campaignId, templateId, messageId);
IterableApi.sharedInstance.setAttributionInfo(attributionInfo);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.iterable.iterableapi;

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

import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;

final class IterableDeeplinkRedirectResolver {
static final int DEFAULT_TIMEOUT_MS = 3000;

private static final String TAG = "RedirectTask";
private static final String SET_COOKIE_HEADER = "Set-Cookie";
private static final String CAMPAIGN_ID_COOKIE = "iterableEmailCampaignId";
private static final String TEMPLATE_ID_COOKIE = "iterableTemplateId";
private static final String MESSAGE_ID_COOKIE = "iterableMessageId";

interface ConnectionFactory {
HttpURLConnection open(String url) throws Exception;
}

private final ConnectionFactory connectionFactory;

IterableDeeplinkRedirectResolver() {
this(url -> (HttpURLConnection) new URL(url).openConnection());
}

IterableDeeplinkRedirectResolver(ConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}

@NonNull
IterableDeeplinkRedirectResult resolve(@NonNull String url) {
HttpURLConnection connection = null;
try {
connection = openConnection(url);
return resolveResponse(url, connection);
} catch (Exception e) {
IterableLogger.e(TAG, e.getMessage());
return new IterableDeeplinkRedirectResult(url);
} finally {
if (connection != null) {
connection.disconnect();
}
}
}

private HttpURLConnection openConnection(String url) throws Exception {
HttpURLConnection connection = connectionFactory.open(url);
connection.setConnectTimeout(DEFAULT_TIMEOUT_MS);
connection.setReadTimeout(DEFAULT_TIMEOUT_MS);
connection.setInstanceFollowRedirects(false);
return connection;
}

private IterableDeeplinkRedirectResult resolveResponse(
String originalUrl,
HttpURLConnection connection
) throws Exception {
int responseCode = connection.getResponseCode();
if (responseCode >= 400) {
IterableLogger.d(
TAG,
"Invalid Request for: " + originalUrl + ", returned code " + responseCode
);
return new IterableDeeplinkRedirectResult(originalUrl);
}
if (responseCode >= 300) {
return resolveRedirectResponse(originalUrl, connection);
}
return new IterableDeeplinkRedirectResult(originalUrl);
}

private IterableDeeplinkRedirectResult resolveRedirectResponse(
String originalUrl,
HttpURLConnection connection
) {
String redirectUrl = connection.getHeaderField(
IterableConstants.LOCATION_HEADER_FIELD
);
if (redirectUrl == null || redirectUrl.trim().isEmpty()) {
return new IterableDeeplinkRedirectResult(originalUrl);
}

RedirectAttribution attribution = parseAttributionCookies(connection);
return new IterableDeeplinkRedirectResult(
redirectUrl,
attribution.campaignId,
attribution.templateId,
attribution.messageId
);
}

private RedirectAttribution parseAttributionCookies(HttpURLConnection connection) {
int campaignId = 0;
int templateId = 0;
String messageId = null;

try {
Map<String, List<String>> headerFields = connection.getHeaderFields();
List<String> cookies = headerFields.get(SET_COOKIE_HEADER);
if (cookies != null) {
for (String header : cookies) {
for (HttpCookie cookie : HttpCookie.parse(header)) {
switch (cookie.getName()) {
case CAMPAIGN_ID_COOKIE:
campaignId = Integer.parseInt(cookie.getValue());
break;
case TEMPLATE_ID_COOKIE:
templateId = Integer.parseInt(cookie.getValue());
break;
case MESSAGE_ID_COOKIE:
messageId = cookie.getValue();
break;
default:
break;
}
}
}
}
} catch (Exception e) {
IterableLogger.e(TAG, "Error while parsing cookies: " + e.getMessage());
}

return new RedirectAttribution(campaignId, templateId, messageId);
}

private static final class RedirectAttribution {
final int campaignId;
final int templateId;
final String messageId;

RedirectAttribution(int campaignId, int templateId, String messageId) {
this.campaignId = campaignId;
this.templateId = templateId;
this.messageId = messageId;
}
}
}

final class IterableDeeplinkRedirectResult {
@Nullable
final String url;
final int campaignId;
final int templateId;
@Nullable
final String messageId;

IterableDeeplinkRedirectResult(@Nullable String url) {
this(url, 0, 0, null);
}

IterableDeeplinkRedirectResult(
@Nullable String url,
int campaignId,
int templateId,
@Nullable String messageId
) {
this.url = url;
this.campaignId = campaignId;
this.templateId = templateId;
this.messageId = messageId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.iterable.iterableapi;

import androidx.annotation.Nullable;

import java.util.concurrent.Executor;

final class IterableDeeplinkRedirectTask implements Runnable {
private final String url;
@Nullable
private final IterableHelper.IterableActionHandler callback;
private final IterableDeeplinkRedirectResolver redirectResolver;
private final Executor callbackExecutor;

IterableDeeplinkRedirectTask(
String url,
@Nullable IterableHelper.IterableActionHandler callback,
IterableDeeplinkRedirectResolver redirectResolver,
Executor callbackExecutor
) {
this.url = url;
this.callback = callback;
this.redirectResolver = redirectResolver;
this.callbackExecutor = callbackExecutor;
}

@Override
public void run() {
IterableDeeplinkRedirectResult result = redirectResolver.resolve(url);
callbackExecutor.execute(() -> deliver(result));
}

private void deliver(IterableDeeplinkRedirectResult result) {
if (callback != null) {
callback.execute(result.url);
}

if (result.campaignId != 0) {
IterableAttributionInfo attributionInfo = new IterableAttributionInfo(
result.campaignId,
result.templateId,
result.messageId
);
IterableApi.sharedInstance.setAttributionInfo(attributionInfo);
}
}
}
Loading
Loading