diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..fb970f2bd --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @frankus @twinklesharma1311 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f163ba82..d62e1e8d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,18 @@ +# 2021-07-27 - v5.6.4 + +#### Fixes + +* Prevent Clipboard Notifications for Release builds in Android 12. + +#### Improvements + +* Replace JCenter with Maven Central. + # 2021-07-07 - v5.6.3 #### Fixes -* Fixes a race condition where information about the SDK and app was failing to be recorded. +* Fixes a race condition where information about the SDK and app was failing to be recorded. # 2021-01-28 - v5.6.2 diff --git a/README.md b/README.md index 1c734af96..66b6dbdef 100644 --- a/README.md +++ b/README.md @@ -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.6.3|aar) +##### Binary releases are hosted for Maven [here](http://search.maven.org/#artifactdetails|com.apptentive|apptentive-android|5.6.4|aar) #### Reporting Bugs diff --git a/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/logic/DefaultRandomPercentProviderTest.java b/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/logic/DefaultRandomPercentProviderTest.java new file mode 100644 index 000000000..412ef3d6e --- /dev/null +++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/logic/DefaultRandomPercentProviderTest.java @@ -0,0 +1,27 @@ +package com.apptentive.android.sdk.module.engagement.logic; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class DefaultRandomPercentProviderTest { +// private final Context context = InstrumentationRegistry.getInstrumentation().getContext(); +// +// @Before +// public void before() { +// DefaultRandomPercentProvider.clear(context); +// } +// +// @Test +// public void testRandomPercent() { +// final RandomPercentProvider provider = new DefaultRandomPercentProvider(context, "id"); +// final double percent1 = provider.getPercent("key"); +// final double percent2 = provider.getPercent("key"); +// assertEquals(percent1, percent2, 0.0000001f); +// } +} \ No newline at end of file diff --git a/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/logic/FieldManagerTest.java b/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/logic/FieldManagerTest.java new file mode 100644 index 000000000..ee0dc30b5 --- /dev/null +++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/logic/FieldManagerTest.java @@ -0,0 +1,70 @@ +package com.apptentive.android.sdk.module.engagement.logic; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.apptentive.android.sdk.storage.AppRelease; +import com.apptentive.android.sdk.storage.Device; +import com.apptentive.android.sdk.storage.EventData; +import com.apptentive.android.sdk.storage.Person; +import com.apptentive.android.sdk.storage.VersionHistory; + +import org.junit.Test; + +import java.math.BigDecimal; + +import static org.junit.Assert.assertEquals; + +public class FieldManagerTest { +// private final Context context = InstrumentationRegistry.getInstrumentation().getContext(); +// +// @Test +// public void testRandomPercentWithKey() { +// final FieldManager fieldManager = createFieldManager(50); +// final Comparable expected = new BigDecimal(50); +// final Comparable actual = fieldManager.getValue("random/abc123xyz/percent"); +// assertEquals(expected, actual); +// } +// +// @Test +// public void testRandomPercentWithoutKey() { +// final FieldManager fieldManager = createFieldManager(50); +// final Comparable expected = new BigDecimal(50); +// final Comparable actual = fieldManager.getValue("random/percent"); +// assertEquals(expected, actual); +// } +// +// @Test +// public void testRandomPercentWithKeyDescription() { +// final FieldManager fieldManager = createFieldManager(50); +// final String expected = "random percent for key 'abc123xyz'"; +// final String actual = fieldManager.getDescription("random/abc123xyz/percent"); +// assertEquals(expected, actual); +// } +// +// @Test +// public void testRandomPercentWithoutKeyDescription() { +// final FieldManager fieldManager = createFieldManager(50); +// final String expected = "random percent"; +// final String actual = fieldManager.getDescription("random/percent"); +// assertEquals(expected, actual); +// } +// +// private FieldManager createFieldManager(double percent) { +// return new FieldManager(context, new VersionHistory(), new EventData(), new Person(), new Device(), new AppRelease(), new MockRandomPercentProvider(percent)); +// } +// +// private static class MockRandomPercentProvider implements RandomPercentProvider { +// private final double percent; +// +// private MockRandomPercentProvider(double percent) { +// this.percent = percent; +// } +// +// @Override +// public double getPercent(String key) { +// return percent; +// } +// } +} \ No newline at end of file diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/conversation/ConversationProxy.java b/apptentive/src/main/java/com/apptentive/android/sdk/conversation/ConversationProxy.java index 6430c1445..fd019f83b 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/conversation/ConversationProxy.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/conversation/ConversationProxy.java @@ -202,5 +202,9 @@ public String getConversationToken() { return conversation.getConversationToken(); } - //endregion +// public String getLocalIdentifier() { +// return conversation.getLocalIdentifier(); +// } + + //endregion } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogMonitor.java b/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogMonitor.java index 213b13f75..a383385d1 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogMonitor.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogMonitor.java @@ -88,6 +88,12 @@ private static void startSessionGuarded(final Context context, String appKey, St return; } + // don't try to read the the pasteboard if the system forbids it + if (!Util.canAccessClipboard(context)) { + ApptentiveLog.w(TROUBLESHOOT, "Unable to access device pasteboard"); + return; + } + // attempt to create a new session based on the clipboard content final String accessToken = readAccessTokenFromClipboard(context); diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/NoteFragment.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/NoteFragment.java index ababe9da0..2244b6ca8 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/NoteFragment.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/NoteFragment.java @@ -26,7 +26,9 @@ import com.apptentive.android.sdk.module.engagement.interaction.model.common.Action; import com.apptentive.android.sdk.module.engagement.interaction.model.common.Actions; import com.apptentive.android.sdk.module.engagement.interaction.model.common.LaunchInteractionAction; +import com.apptentive.android.sdk.module.engagement.logic.DefaultRandomPercentProvider; import com.apptentive.android.sdk.module.engagement.logic.FieldManager; +import com.apptentive.android.sdk.module.engagement.logic.RandomPercentProvider; import org.json.JSONException; import org.json.JSONObject; @@ -127,8 +129,9 @@ public void onClick(View view) { LaunchInteractionAction launchInteractionButton = (LaunchInteractionAction) buttonAction; List invocations = launchInteractionButton.getInvocations(); String interactionIdToLaunch = null; + //final RandomPercentProvider percentProvider = new DefaultRandomPercentProvider(getContext(), getConversation().getLocalIdentifier()); for (Invocation invocation : invocations) { - FieldManager fieldManager = new FieldManager(getContext(), getConversation().getVersionHistory(), getConversation().getEventData(), getConversation().getPerson(), getConversation().getDevice(), getConversation().getAppRelease()); + FieldManager fieldManager = new FieldManager(getContext(), getConversation().getVersionHistory(), getConversation().getEventData(), getConversation().getPerson(), getConversation().getDevice(), getConversation().getAppRelease()/*,percentProvider*/); if (invocation.isCriteriaMet(fieldManager, false)) { // TODO: should we print details here as well? interactionIdToLaunch = invocation.getInteractionId(); break; diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Targets.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Targets.java index ee65a7d95..cd5cfee45 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Targets.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Targets.java @@ -6,10 +6,14 @@ package com.apptentive.android.sdk.module.engagement.interaction.model; +import android.content.Context; + import com.apptentive.android.sdk.ApptentiveInternal; import com.apptentive.android.sdk.ApptentiveLog; import com.apptentive.android.sdk.conversation.Conversation; +import com.apptentive.android.sdk.module.engagement.logic.DefaultRandomPercentProvider; import com.apptentive.android.sdk.module.engagement.logic.FieldManager; +import com.apptentive.android.sdk.module.engagement.logic.RandomPercentProvider; import org.json.JSONArray; import org.json.JSONException; @@ -38,7 +42,9 @@ public String getApplicableInteraction(String eventLabel, boolean verbose) { try { Invocation invocation = new Invocation(invocationObject.toString()); Conversation conversation = ApptentiveInternal.getInstance().getConversation(); - FieldManager fieldManager = new FieldManager(ApptentiveInternal.getInstance().getApplicationContext(), conversation.getVersionHistory(), conversation.getEventData(), conversation.getPerson(), conversation.getDevice(), conversation.getAppRelease()); + final Context context = ApptentiveInternal.getInstance().getApplicationContext(); + //final RandomPercentProvider percentProvider = new DefaultRandomPercentProvider(context, conversation.getLocalIdentifier()); + FieldManager fieldManager = new FieldManager(context, conversation.getVersionHistory(), conversation.getEventData(), conversation.getPerson(), conversation.getDevice(), conversation.getAppRelease()/*,percentProvider*/); if (invocation.isCriteriaMet(fieldManager, verbose)) { return invocation.getInteractionId(); diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/DefaultRandomPercentProvider.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/DefaultRandomPercentProvider.java new file mode 100644 index 000000000..a767885c6 --- /dev/null +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/DefaultRandomPercentProvider.java @@ -0,0 +1,69 @@ +package com.apptentive.android.sdk.module.engagement.logic; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.VisibleForTesting; + +import com.apptentive.android.sdk.util.ApplicationInfo; +import com.apptentive.android.sdk.util.RuntimeUtils; + +import java.util.Random; + +/** + * A concrete implementation of [RandomPercentProvider] which only generates a random percent for a + * give key once and stores it in shared preferences. + */ +public class DefaultRandomPercentProvider implements RandomPercentProvider { +// private final Context context; +// private final String id; +// +// /** +// * @param id - unique key for making a distinction between same keys used in different conversations. +// */ +// public DefaultRandomPercentProvider(Context context, String id) { +// if (context == null) { +// throw new IllegalArgumentException("Context is null"); +// } +// if (id == null) { +// throw new IllegalArgumentException("Id is null"); +// } +// this.context = context.getApplicationContext(); +// this.id = id; +// } +// +// @Override +// public double getPercent(String key) { +// final SharedPreferences prefs = getPrefs(context); +// if (key == null) { +// return getRandomPercent(); +// } else { +// final String prefsKey = id + "_" + key; +// if (prefs.contains(prefsKey)) { +// return prefs.getFloat(prefsKey, 0.0f); +// } +// final float percent = getRandomPercent(); +// prefs.edit().putFloat(prefsKey, percent).apply(); +// return percent; +// } +// } +// +// private float getRandomPercent() { +// ApplicationInfo applicationInfo = RuntimeUtils.getApplicationInfo(context); +// +// if (applicationInfo.isDebuggable()) { +// return (float) 50; +// } else { +// return new Random().nextFloat() * 100; +// } +// } +// +// private static SharedPreferences getPrefs(Context context) { +// return context.getSharedPreferences("com.apptentive.RandomPercentProvider", Context.MODE_PRIVATE); +// } +// +// @VisibleForTesting +// public static void clear(Context context) { +// getPrefs(context).edit().clear().apply(); +// } +} diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/FieldManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/FieldManager.java index 9f03aaec9..c83aae414 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/FieldManager.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/FieldManager.java @@ -10,7 +10,6 @@ import com.apptentive.android.sdk.Apptentive; import com.apptentive.android.sdk.ApptentiveLog; -import com.apptentive.android.sdk.ApptentiveLogTag; import com.apptentive.android.sdk.debug.Assert; import com.apptentive.android.sdk.storage.AppRelease; import com.apptentive.android.sdk.storage.CustomData; @@ -29,26 +28,28 @@ import static com.apptentive.android.sdk.debug.ErrorMetrics.logException; public class FieldManager { - Context context; VersionHistory versionHistory; EventData eventData; Person person; Device device; AppRelease appRelease; + //private final RandomPercentProvider randomPercentProvider; - public FieldManager(Context context, VersionHistory versionHistory, EventData eventData, Person person, Device device, AppRelease appRelease) { + public FieldManager(Context context, VersionHistory versionHistory, EventData eventData, Person person, Device device, AppRelease appRelease/*,RandomPercentProvider randomPercentProvider*/) { Assert.notNull(context); Assert.notNull(versionHistory); Assert.notNull(eventData); Assert.notNull(person); Assert.notNull(device); + //Assert.notNull(randomPercentProvider); this.context = context; this.versionHistory = versionHistory; this.eventData = eventData; this.person = person; this.device = device; this.appRelease = appRelease; + //this.randomPercentProvider = randomPercentProvider; } public boolean exists(String query) { @@ -61,7 +62,6 @@ public Comparable getValue(String query) { } private Object doGetValue(String query) { - query = query.trim(); String[] tokens = query.split("/"); QueryPart topLevelQuery = QueryPart.parse(tokens[0]); @@ -289,6 +289,22 @@ private Object doGetValue(String query) { return null; } } +// case random: { +// if (tokens.length == 3) { // random//percent +// final String randomNumberKey = tokens[1]; +// QueryPart subQuery = QueryPart.valueOf(tokens[2]); +// switch (subQuery) { +// case percent: +// return randomPercentProvider.getPercent(randomNumberKey); +// } +// } else if (tokens.length == 2) { // random/percent +// QueryPart subQuery = QueryPart.valueOf(tokens[1]); +// switch (subQuery) { +// case percent: +// return randomPercentProvider.getPercent(null); +// } +// } +// } default: break; } @@ -490,6 +506,22 @@ public String getDescription(String query) { return null; } } +// case random: { +// if (tokens.length == 3) { // random//percent +// final String randomNumberKey = tokens[1]; +// QueryPart subQuery = QueryPart.valueOf(tokens[2]); +// switch (subQuery) { +// case percent: +// return StringUtils.format("random percent for key '%s'", randomNumberKey); +// } +// } else if (tokens.length == 2) { // random/percent +// QueryPart subQuery = QueryPart.valueOf(tokens[1]); +// switch (subQuery) { +// case percent: +// return StringUtils.format("random percent"); +// } +// } +// } default: break; } @@ -544,6 +576,10 @@ private enum QueryPart { debug, build, time_ago, + +// random, +// percent, + other; public static QueryPart parse(String name) { diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/RandomPercentProvider.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/RandomPercentProvider.java new file mode 100644 index 000000000..3107a7517 --- /dev/null +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/RandomPercentProvider.java @@ -0,0 +1,11 @@ +package com.apptentive.android.sdk.module.engagement.logic; + +/** + * Represents an object which returns a random percent for a give key. + */ +public interface RandomPercentProvider { +// /** +// * Returns a random percent for a give key in range from [0..100] +// */ +// double getPercent(String key); +} \ No newline at end of file diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/NoteInteractionNotificationAdapter.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/NoteInteractionNotificationAdapter.java index bf3840702..6ef119461 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/NoteInteractionNotificationAdapter.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/NoteInteractionNotificationAdapter.java @@ -29,7 +29,9 @@ import com.apptentive.android.sdk.module.engagement.interaction.model.common.Action; import com.apptentive.android.sdk.module.engagement.interaction.model.common.Actions; import com.apptentive.android.sdk.module.engagement.interaction.model.common.LaunchInteractionAction; +import com.apptentive.android.sdk.module.engagement.logic.DefaultRandomPercentProvider; import com.apptentive.android.sdk.module.engagement.logic.FieldManager; +import com.apptentive.android.sdk.module.engagement.logic.RandomPercentProvider; import com.apptentive.android.sdk.util.Constants; import com.apptentive.android.sdk.util.StringUtils; import com.apptentive.android.sdk.util.Util; @@ -205,8 +207,9 @@ protected boolean execute(Conversation conversation) { String interactionIdToLaunch = null; // Need to check each Invocation object's criteria to find the right one. + //final RandomPercentProvider percentProvider = new DefaultRandomPercentProvider(context, conversation.getLocalIdentifier()); for (Invocation invocation : invocations) { - FieldManager fieldManager = new FieldManager(context, conversation.getVersionHistory(), conversation.getEventData(), conversation.getPerson(), conversation.getDevice(), conversation.getAppRelease()); + FieldManager fieldManager = new FieldManager(context, conversation.getVersionHistory(), conversation.getEventData(), conversation.getPerson(), conversation.getDevice(), conversation.getAppRelease()/*,percentProvider*/); if (invocation.isCriteriaMet(fieldManager, true)) { interactionIdToLaunch = invocation.getInteractionId(); ApptentiveLog.v(NOTIFICATION_INTERACTIONS, "Found an Interaction to launch with id %s", interactionIdToLaunch); diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/Constants.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/Constants.java index d7ad947f6..ee3e0591b 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/util/Constants.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/Constants.java @@ -9,7 +9,7 @@ public class Constants { public static final int API_VERSION = 10; - private static final String APPTENTIVE_SDK_VERSION = "5.6.3"; + private static final String APPTENTIVE_SDK_VERSION = "5.6.4"; public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 45000; public static final int DEFAULT_READ_TIMEOUT_MILLIS = 45000; diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/Util.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/Util.java index 1dc4b68c9..6d37b02f7 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/util/Util.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/Util.java @@ -54,6 +54,7 @@ import com.apptentive.android.sdk.ApptentiveInternal; import com.apptentive.android.sdk.ApptentiveLog; +import com.apptentive.android.sdk.ApptentiveLogTag; import com.apptentive.android.sdk.R; import com.apptentive.android.sdk.model.StoredFile; import com.apptentive.android.sdk.util.threading.DispatchQueue; @@ -84,6 +85,7 @@ import java.text.SimpleDateFormat; import java.util.*; +import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION; import static com.apptentive.android.sdk.ApptentiveLogTag.UTIL; import static com.apptentive.android.sdk.debug.ErrorMetrics.logException; @@ -221,14 +223,6 @@ public static boolean isEmailValid(String email) { return !StringUtils.isNullOrEmpty(email) && Patterns.EMAIL_ADDRESS.matcher(email).matches(); } - public static boolean getPackageMetaDataBoolean(Context context, String key) { - try { - return context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA).metaData.getBoolean(key, false); - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } - public static Object getPackageMetaData(Context appContext, String key) { try { return appContext.getPackageManager().getApplicationInfo(appContext.getPackageName(), PackageManager.GET_META_DATA).metaData.get(key); @@ -1169,6 +1163,20 @@ public static String getManifestMetadataString(Context context, String key) { return null; } + /* + * On Android 12 and up the user would see a toast notification when the app tries to access the + * pasteboard. We need to prevent this from happening and only check pasteboard in earlier + * versions or debug configurations. + */ + public static boolean canAccessClipboard(Context context) { + try { + return Build.VERSION.SDK_INT <= Build.VERSION_CODES.R || RuntimeUtils.getApplicationInfo(context).isDebuggable(); + } catch (Exception e) { + ApptentiveLog.e(UTIL, e, "Unable to determine if the clipboard can be accessed"); + return false; + } + } + public static String getClipboardText(Context context) { if (context == null) { throw new IllegalArgumentException("Context is null"); diff --git a/build.gradle b/build.gradle index e9296f2c4..09455bf22 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { repositories { google() // "https://maven.google.com" - jcenter() + mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:3.5.2' @@ -15,6 +15,6 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } }