Skip to content

Commit

Permalink
Merge pull request #215 from apptentive/branch_5.6.0
Browse files Browse the repository at this point in the history
Release 5.6.0
  • Loading branch information
weeeBox authored Sep 28, 2020
2 parents 153042a + f5046ef commit ad6a71e
Show file tree
Hide file tree
Showing 26 changed files with 721 additions and 36 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 2020-09-28 - v5.6.0

#### Improvements

* Google Play In-App Review support.

# 2020-09-04 - v5.5.4

#### Fixes
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use your app, to talk to them at the right time, and in the right way.

##### [Release Notes](https://learn.apptentive.com/knowledge-base/android-sdk-release-notes/)

##### Binary releases are hosted for Maven [here](http://search.maven.org/#artifactdetails|com.apptentive|apptentive-android|5.5.4|aar)
##### Binary releases are hosted for Maven [here](http://search.maven.org/#artifactdetails|com.apptentive|apptentive-android|5.6.0|aar)

#### Reporting Bugs

Expand Down
6 changes: 6 additions & 0 deletions apptentive/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.google.android.material:material:1.2.0'

// Play Core library required for in-app review flow
implementation 'com.google.android.play:core:1.8.0'

// Play Services library required to check for GPS availability before showing in-app review
implementation 'com.google.android.gms:play-services-base:17.4.0'

testImplementation 'junit:junit:4.13'
testImplementation 'org.powermock:powermock-module-junit4:1.6.6'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.6'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package com.apptentive.android.sdk.module.engagement;

import android.content.Context;

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

import com.apptentive.android.sdk.InstrumentationTestCaseBase;
import com.apptentive.android.sdk.external.Engagement;
import com.apptentive.android.sdk.external.InAppReviewListener;
import com.apptentive.android.sdk.external.InAppReviewManager;
import com.apptentive.android.sdk.external.InAppReviewManagerFactory;
import com.apptentive.android.sdk.module.engagement.interaction.model.InAppRatingDialogInteraction;
import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
import com.apptentive.android.sdk.util.Callback;
import com.apptentive.android.sdk.util.StringUtils;

import junit.framework.AssertionFailedError;

import org.json.JSONException;
import org.junit.Test;

import java.util.Map;

public class InAppRatingDialogInteractionLauncherTest extends InstrumentationTestCaseBase {
private static final String FALLBACK_INTERACTION_ID = "1234567";

private static final String JSON = "{'type':'InAppRatingDialog','version':1,'configuration':{'not_shown_interaction': '" + FALLBACK_INTERACTION_ID + "'},'display_type':null,'id':'58eebbf2127096704e0000d0'}";
private static final String JSON_MISSING_FALLBACK = "{'type':'InAppRatingDialog','version':1,'configuration':{'not_shown_interaction': '0000000'},'display_type':null,'id':'58eebbf2127096704e0000d0'}";
private static final String JSON_NO_FALLBACK = "{'type':'InAppRatingDialog','version':1,'configuration':{},'display_type':null,'id':'58eebbf2127096704e0000d0'}";

//region In-App Review supported

@Test
public void testInAppReviewSupportedLaunchSucceed() {
InAppRatingDialogInteractionLauncher launcher = createLauncher(MockInAppReviewManager.successful());
launcher.launch(getContext(), createInteraction(JSON));

assertResult(
"engage=InAppRatingDialog#request",
"engage=InAppRatingDialog#shown"
);
}

@Test
public void testInAppReviewSupportedLaunchFailed() {
InAppRatingDialogInteractionLauncher launcher = createLauncher(MockInAppReviewManager.failed("Something went wrong"));
launcher.launch(getContext(), createInteraction(JSON));

assertResult(
"engage=InAppRatingDialog#request",
"engage=InAppRatingDialog#not_shown data='cause':'Something went wrong'"
);
}

//endregion

//region In-App Review unsupported

@Test
public void testInAppReviewUnsupportedWithFallbackAction() {
InAppRatingDialogInteractionLauncher launcher = createLauncher(MockInAppReviewManager.unsupported());
launcher.launch(getContext(), createInteraction(JSON));

assertResult(
"engage=InAppRatingDialog#request",
"engage=InAppRatingDialog#not_supported",
"launch=1234567", // fallback interaction
"engage=InAppRatingDialog#launch"
);
}

@Test
public void testInAppReviewUnsupportedWithNoFallbackAction() {
InAppRatingDialogInteractionLauncher launcher = createLauncher(MockInAppReviewManager.unsupported());
launcher.launch(getContext(), createInteraction(JSON_NO_FALLBACK));

assertResult(
"engage=InAppRatingDialog#request",
"engage=InAppRatingDialog#not_supported"
);
}

@Test
public void testInAppReviewUnsupportedWithMissingFallbackAction() {
InAppRatingDialogInteractionLauncher launcher = createLauncher(MockInAppReviewManager.unsupported());
launcher.launch(getContext(), createInteraction(JSON_MISSING_FALLBACK));

assertResult(
"engage=InAppRatingDialog#request",
"engage=InAppRatingDialog#not_supported"
);
}

//endregion

//region Helpers

private InAppRatingDialogInteraction createInteraction(String json) {
try {
return new InAppRatingDialogInteraction(json.replace("'", "\""));
} catch (JSONException e) {
throw new AssertionFailedError(e.getMessage());
}
}

private InAppRatingDialogInteractionLauncher createLauncher(InAppReviewManager manager) {
Engagement engagement = new Engagement() {
@Override
public void engageInternal(Context context, Interaction interaction, String eventName, @Nullable Map<String, Object> data) {
String event = interaction.getType() + "#" + eventName;
if (data != null) {
addResult("engage=%s data=%s", event, StringUtils.toString(data));
} else {
addResult("engage=%s", event);
}
}

@Override
public void launchInteraction(Context context, String interactionId, Callback<Boolean> callback) {
if (FALLBACK_INTERACTION_ID.equals(interactionId)) {
addResult("launch=%s", interactionId);
callback.onFinish(Boolean.TRUE);
} else {
callback.onFinish(Boolean.FALSE);
}
}
};

return new InAppRatingDialogInteractionLauncher(new MockInAppReviewManagerFactory(manager), engagement);
}

//endregion

//region Mocks

private static class MockInAppReviewManager implements InAppReviewManager {
private final String errorMessage;
private final boolean supported;

private MockInAppReviewManager(boolean supported, String errorMessage) {
this.errorMessage = errorMessage;
this.supported = supported;
}

public static InAppReviewManager successful() {
return new MockInAppReviewManager(true, null);
}

public static InAppReviewManager failed(String errorMessage) {
return new MockInAppReviewManager(true, errorMessage);
}

public static InAppReviewManager unsupported() {
return new MockInAppReviewManager(false, null);
}

@Override
public void launchReview(@NonNull Context context, InAppReviewListener callback) {
if (supported) {
if (errorMessage != null) {
callback.onReviewFlowFailed(errorMessage);
} else {
callback.onReviewFlowComplete();
}
} else {
throw new AssertionFailedError("Should not get there");
}
}

@Override
public boolean isInAppReviewSupported() {
return supported;
}
}

private static class MockInAppReviewManagerFactory implements InAppReviewManagerFactory {
private final InAppReviewManager manager;

private MockInAppReviewManagerFactory(InAppReviewManager manager) {
this.manager = manager;
}

@Override
public InAppReviewManager createReviewManager(@NonNull Context context) {
return manager;
}
}

private static class MockInteraction extends Interaction {
public static Interaction create(String json) {
try {
return new MockInteraction(json);
} catch (JSONException e) {
throw new AssertionFailedError(e.getMessage());
}
}

private MockInteraction(String json) throws JSONException {
super(json);
}
}

//endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import androidx.test.runner.AndroidJUnit4;

import com.apptentive.android.sdk.InstrumentationTestCaseBase;
import com.apptentive.android.sdk.module.engagement.interaction.model.InAppRatingDialogInteraction;
import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction.DisplayType;
import com.apptentive.android.sdk.module.engagement.interaction.model.TextModalInteraction;
Expand Down Expand Up @@ -43,6 +44,12 @@ protected InteractionLauncher createActivityInteractionLauncher() {
protected InteractionLauncher createNotificationInteractionLauncher() {
return new MockInteractionLauncher("Notification");
}

@NonNull
@Override
InteractionLauncher createInAppRatingDialogInteractionLauncher() {
return new MockInteractionLauncher("InAppReview");
}
});

// Everything should run immediately
Expand All @@ -65,6 +72,14 @@ public void testInteractionNotificationDisplayType() throws JSONException {
assertResult("Notification");
}

@Test
public void testInteractionInAppReview() throws JSONException {
Interaction interaction = new InAppRatingDialogInteraction("{\"type\":\"InAppRatingDialog\"}");
assertEquals(interaction.getType(), Interaction.Type.InAppRatingDialog);
EngagementModule.launchInteraction(getContext(), interaction);
assertResult("InAppReview");
}

@Test
public void testInteractionUnknownDisplayType() throws JSONException {
Interaction interaction = new TextModalInteraction("{\"type\":\"TextModal\",\"display_Type\":\"unknown\"}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,11 @@ private static void logGuarded(Level level, ApptentiveLogTag tag, Throwable thro
message = extra.append(' ').append(message).toString();
}

log0(level, message);
if (throwable != null) {
log0(level, StringUtils.getStackTrace(throwable));
message += '\n' + StringUtils.getStackTrace(throwable);
}

log0(level, message);
}

private static void log0(Level level, String message) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ public enum ApptentiveLogTag {
TROUBLESHOOT,
ADVERTISER_ID,
PARTNERS,
SECURITY
SECURITY,
IN_APP_REVIEW
}
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,7 @@ public Interaction getApplicableInteraction(String eventLabel, boolean verbose)
Targets targets = new Targets(getTargets());
String interactionId = targets.getApplicableInteraction(eventLabel, verbose);
if (interactionId != null) {
String interactionsString = getInteractions();
if (interactionsString != null) {
Interactions interactions = new Interactions(interactionsString);
return interactions.getInteraction(interactionId);
}
return getInteraction(interactionId);
}
} catch (JSONException e) {
ApptentiveLog.e(INTERACTIONS, e, "Exception while getting applicable interaction: %s", eventLabel);
Expand All @@ -229,6 +225,18 @@ public Interaction getApplicableInteraction(String eventLabel, boolean verbose)
return null;
}

/**
* Returns an Interaction for <code>interactionId</code> if there is one.
*/
public @Nullable Interaction getInteraction(@NonNull String interactionId) throws JSONException {
String interactionsString = getInteractions();
if (interactionsString != null) {
Interactions interactions = new Interactions(interactionsString);
return interactions.getInteraction(interactionId);
}
return null;
}

public void fetchInteractions(Context context) {
if (!isPollForInteractions()) {
ApptentiveLog.d(CONVERSATION, "Interaction polling is turned off. Skipping fetch.");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.apptentive.android.sdk.external;

import android.content.Context;

import androidx.annotation.Nullable;

import com.apptentive.android.sdk.ApptentiveLog;
import com.apptentive.android.sdk.ApptentiveLogTag;
import com.apptentive.android.sdk.conversation.Conversation;
import com.apptentive.android.sdk.conversation.ConversationDispatchTask;
import com.apptentive.android.sdk.module.engagement.EngagementModule;
import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
import com.apptentive.android.sdk.util.Callback;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.Map;

import static com.apptentive.android.sdk.ApptentiveHelper.dispatchConversationTask;

public class DefaultEngagement implements Engagement {
@Override
public void engageInternal(final Context context, final Interaction interaction, final String eventName, @Nullable final Map<String, Object> data) {
dispatchConversationTask(new ConversationDispatchTask() {
@Override
protected boolean execute(Conversation conversation) {
String jsonData = createJsonData(data);
return EngagementModule.engageInternal(context, conversation, interaction, eventName, jsonData);
}
}, "engage event '" + eventName + "'");
}

@Override
public void launchInteraction(final Context context, final String interactionId, final Callback<Boolean> callback) {
dispatchConversationTask(new ConversationDispatchTask() {
@Override
protected boolean execute(Conversation conversation) {
Interaction interaction = getInteraction(conversation);
if (interaction != null) {
EngagementModule.launchInteraction(context, conversation, interaction);
callback.onFinish(Boolean.TRUE);
} else {
callback.onFinish(Boolean.FALSE);
}

return true;
}

private @Nullable Interaction getInteraction(Conversation conversation) {
try {
return conversation.getInteraction(interactionId);
} catch (JSONException e) {
ApptentiveLog.e(ApptentiveLogTag.CONVERSATION, e, "Unable to get interaction '%s'", interactionId);
}
return null;
}
}, "launch interaction '" + interactionId + "'");
}

private static String createJsonData(Map<String, Object> data) {
if (data != null && data.size() > 0) {
return new JSONObject(data).toString();
}

return null;
}
}
Loading

0 comments on commit ad6a71e

Please sign in to comment.