Skip to content

Commit

Permalink
Release/6.5.0 (#23)
Browse files Browse the repository at this point in the history
Releasing 6.5
  • Loading branch information
PoornimaApptentive authored Nov 14, 2023
1 parent 0b19f2c commit c9e1163
Show file tree
Hide file tree
Showing 139 changed files with 5,106 additions and 752 deletions.
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# https://help.github.com/en/articles/about-code-owners

* @PoornimaApptentive @ChaseApptentive
* @PoornimaApptentive @frankus
28 changes: 21 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# 2023-11-14 - v6.5.0
#### New Features
* Implemented Customer Authentication features from the legacy SDK in the new SDK (See Android [Integration guide](https://learn.apptentive.com/knowledge-base/android-integration-guide/)). This allows apps with sensitive data to be shared among multiple users on a single device
* Added the ability to work with multiple app key/signature pairs without deleting and reinstalling

#### Improvements
* Launch and exit events are standardized for app launches and exits
* Send callback failure if the SDK is registered already
* Expose a method to find if the SDK is already registered
* Confirm that the SDK only makes HTTPS requests

#### Fixes
* Resolved validation issues for free form question type

# 2023-07-20 - v6.1.0
#### New Features
* Survey skip logic
Expand All @@ -21,17 +35,17 @@
* Added session id to the payloads

#### Fixes
* Resolved internal observers and observables issue
* Resolved internal observers and observables issue

#### Known Issues and Limitations
* Client authentication (login/logout) is not yet supported

# 2023-04-05 - v6.0.4
#### Improvements
* Expanded support for links in survey introduction
#### Fixes
* Resolved resource linking issues
* Expanded support for links in survey introduction

#### Fixes
* Resolved resource linking issues

#### Known Issues and Limitations
* Client authentication (login/logout) is not yet supported
Expand All @@ -49,7 +63,7 @@
#### New Features
* Device storage encryption support
* Event observer support to listen for Apptentive events
* Message Center observer support to listen for Message Center updates
* Message Center observer support to listen for Message Center updates

#### Improvements
* `canShowInteraction` function to check if an event will display an interaction
Expand Down Expand Up @@ -104,4 +118,4 @@


#### Previous Releases
You can find versions 5 and earlier in our [legacy SDK repository](https://github.com/apptentive/apptentive-android)
You can find versions 5 and earlier in our [legacy SDK repository](https://github.com/apptentive/apptentive-android)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import android.content.Context
import android.content.pm.PackageManager
import androidx.appcompat.view.ContextThemeWrapper
import apptentive.com.android.R
import apptentive.com.android.core.DependencyProvider
import apptentive.com.android.platform.AndroidSharedPrefDataStore
import apptentive.com.android.platform.SharedPrefConstants
import apptentive.com.android.util.InternalUseOnly
import apptentive.com.android.util.Log
Expand Down Expand Up @@ -69,9 +71,9 @@ fun ContextThemeWrapper.overrideTheme() {
*
* Default is `true` (inherit the host app's theme).
*/
private fun Context.getShouldApplyAppTheme(): Boolean {
return getSharedPreferences(SharedPrefConstants.USE_HOST_APP_THEME, Context.MODE_PRIVATE)
.getBoolean(SharedPrefConstants.USE_HOST_APP_THEME_KEY, true)
private fun getShouldApplyAppTheme(): Boolean {
return DependencyProvider.of<AndroidSharedPrefDataStore>()
.getBoolean(SharedPrefConstants.USE_HOST_APP_THEME, SharedPrefConstants.USE_HOST_APP_THEME_KEY, true)
}

private fun Context.applyAppTheme() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ object DependencyProvider {
lookup.clear()
}

inline fun <reified T> isRegistered() =
lookup[T::class.java] != null

inline fun <reified T> of(): T {
val provider = lookup[T::class.java] ?: throw IllegalArgumentException("Provider is not registered: ${T::class.java}")
return provider.get() as T
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,20 @@ class AESEncryption23(private val keyInfo: EncryptionKey) : Encryption {
}

override fun encrypt(data: ByteArray): ByteArray {
return encrypt(data, true)
}

fun encryptPayloadData(data: ByteArray): ByteArray {
return encrypt(data, false)
}

fun encrypt(data: ByteArray, includeIVLength: Boolean): ByteArray {
val encryptCipher = encryptCipherForIv()
val outputStream = ByteArrayOutputStream()

outputStream.write(encryptCipher.iv.size)
if (includeIVLength) {
outputStream.write(encryptCipher.iv.size)
}
outputStream.write(encryptCipher.iv)

val stream = CipherOutputStream(outputStream, encryptCipher)
Expand All @@ -60,13 +70,22 @@ class AESEncryption23(private val keyInfo: EncryptionKey) : Encryption {

override fun decrypt(data: ByteArray): ByteArray {
val inputStream = ByteArrayInputStream(data)
return decrypt(inputStream)
return decrypt(inputStream, true)
}

override fun decrypt(inputStream: InputStream): ByteArray {
return decrypt(inputStream, true)
}

fun decryptPayloadData(data: ByteArray): ByteArray {
val inputStream = ByteArrayInputStream(data)
return decrypt(inputStream, false)
}

fun decrypt(inputStream: InputStream, decodeIVLength: Boolean): ByteArray {
val outputStream = ByteArrayOutputStream()
inputStream.use { input ->
val ivSize = input.read()
val ivSize = if (decodeIVLength) input.read() else IV_LENGTH
val iv = ByteArray(ivSize)
input.read(iv)
val decryptCipher = decryptCipherForIv(iv)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
package apptentive.com.android.encryption

import android.os.Build
import android.security.keystore.KeyProperties
import androidx.annotation.RequiresApi
import apptentive.com.android.util.InternalUseOnly
import apptentive.com.android.util.hexToBytes
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec

@InternalUseOnly
data class EncryptionKey(val key: SecretKey? = null, val transformation: String = "") {
companion object {
val NO_OP: EncryptionKey = EncryptionKey()
}
}

@RequiresApi(Build.VERSION_CODES.M)
fun String.getKeyFromHexString(): SecretKey {
return SecretKeySpec(hexToBytes(this), KeyProperties.KEY_ALGORITHM_AES)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import apptentive.com.android.util.InternalUseOnly
@InternalUseOnly
interface KeyResolver {
fun resolveKey(): EncryptionKey
fun resolveMultiUserWrapperKey(user: String): EncryptionKey
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import androidx.annotation.RequiresApi
import apptentive.com.android.core.DependencyProvider
import apptentive.com.android.platform.AndroidSharedPrefDataStore
import apptentive.com.android.platform.SharedPrefConstants.CRYPTO_KEY_ALIAS
import apptentive.com.android.platform.SharedPrefConstants.CRYPTO_KEY_WRAPPER_ALIAS
import apptentive.com.android.platform.SharedPrefConstants.SDK_CORE_INFO
import apptentive.com.android.util.InternalUseOnly
import java.security.KeyStore
import java.util.UUID
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey

@RequiresApi(Build.VERSION_CODES.M)
internal class KeyResolver23 : KeyResolver {
@InternalUseOnly
class KeyResolver23 : KeyResolver {
private val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply {
load(null)
}
Expand All @@ -26,6 +29,23 @@ internal class KeyResolver23 : KeyResolver {
return EncryptionKey(getKey(), getTransformation())
}

override fun resolveMultiUserWrapperKey(user: String): EncryptionKey {
return EncryptionKey(getWrapperKey(user), getTransformation())
}

private fun getWrapperKey(user: String): SecretKey {
val keyAlias = androidProxy.getString(SDK_CORE_INFO, user + CRYPTO_KEY_WRAPPER_ALIAS)
val exitingKey = keyStore.getEntry(keyAlias, null) as? KeyStore.SecretKeyEntry

return if (exitingKey?.secretKey == null) {
val newKeyAlias = KEY_ALIAS + UUID.randomUUID()
androidProxy.putString(SDK_CORE_INFO, user + CRYPTO_KEY_WRAPPER_ALIAS, newKeyAlias)
createKey(newKeyAlias)
} else {
exitingKey.secretKey
}
}

@Throws(EncryptionException::class)
private fun getKey(): SecretKey {
val keyAlias = androidProxy.getString(SDK_CORE_INFO, CRYPTO_KEY_ALIAS)
Expand Down Expand Up @@ -61,14 +81,14 @@ internal class KeyResolver23 : KeyResolver {
}
}

private fun getTransformation() = "$ALGORITHM/$BLOCK_MODE/$PADDING"

private companion object {
companion object {
const val KEYSTORE_PROVIDER = "AndroidKeyStore"
const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC
const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
const val KEY_ALIAS = "apptentive-crypto-key-SDK"
const val KEY_LENGTH = 256

fun getTransformation() = "$ALGORITHM/$BLOCK_MODE/$PADDING"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ internal class KeyResolverNoOp : KeyResolver {
override fun resolveKey(): EncryptionKey {
return EncryptionKey.NO_OP
}

override fun resolveMultiUserWrapperKey(user: String): EncryptionKey {
return EncryptionKey.NO_OP
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package apptentive.com.android.network

import android.os.Build
import apptentive.com.android.util.InternalUseOnly
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import java.util.Base64

/** Represent an HTTP-request body */
@InternalUseOnly
Expand All @@ -12,20 +14,37 @@ interface HttpRequestBody {

/** Writes HTTP-request body to an output stream */
fun write(stream: OutputStream)
}

@InternalUseOnly
fun HttpRequestBody.asString(): String {
val stream = ByteArrayOutputStream()
write(stream)
return stream.toByteArray().toString(Charsets.UTF_8)
fun asString(): String {
val stream = ByteArrayOutputStream()
write(stream)
return stream.toByteArray().toString(Charsets.UTF_8)
}
}

internal class BinaryRequestBody(
@InternalUseOnly
class BinaryRequestBody(
private val data: ByteArray,
override val contentType: String
) : HttpRequestBody {
override fun write(stream: OutputStream) {
stream.write(data)
}

override fun asString(): String {
return try {
when {
data.size > 5000 -> "Request body too large to print."
contentType.startsWith("application/json") ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.O -> String(
data,
Charsets.UTF_8
)

else -> "Binary data: ${Base64.getEncoder().encodeToString(data)}"
}
} catch (e: Exception) {
"Error while printing request body: ${e.message}"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,10 @@ import apptentive.com.android.util.InternalUseOnly
* Thrown to indicate an unexpected 400-499 responses.
*/
@InternalUseOnly
class SendErrorException(val statusCode: Int, val statusMessage: String, val errorMessage: String? = null) :
class SendErrorException(
val statusCode: Int,
val statusMessage: String,
val errorType: String? = null,
val errorMessage: String? = null
) :
Exception("Send error: $statusCode ($statusMessage)${if (errorMessage?.isNotEmpty() == true) ": $errorMessage" else ""}")
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import apptentive.com.android.util.InternalUseOnly
interface AndroidSharedPrefDataStore {
fun getSharedPrefForSDK(file: String): SharedPreferences
fun getString(file: String, keyEntry: String, defaultValue: String = ""): String
fun getNullableString(file: String, keyEntry: String, defaultValue: String?): String?
fun getBoolean(file: String, keyEntry: String, defaultValue: Boolean = false): Boolean
fun putString(file: String, keyEntry: String, value: String)
fun getInt(file: String, keyEntry: String, defaultValue: Int = -1): Int
fun putString(file: String, keyEntry: String, value: String?)
fun putBoolean(file: String, keyEntry: String, value: Boolean)
fun getLong(file: String, keyEntry: String, defaultValue: Long = 0): Long
fun putLong(file: String, keyEntry: String, value: Long)
fun containsKey(file: String, keyEntry: String): Boolean
fun putInt(file: String, keyEntry: String, value: Int)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,43 @@ class DefaultAndroidSharedPrefDataStore(val context: Context) : AndroidSharedPre
override fun getBoolean(file: String, keyEntry: String, defaultValue: Boolean): Boolean =
context.getSharedPreferences(file, Context.MODE_PRIVATE).getBoolean(keyEntry, defaultValue)

override fun getInt(file: String, keyEntry: String, defaultValue: Int): Int =
context.getSharedPreferences(file, Context.MODE_PRIVATE).getInt(keyEntry, defaultValue)

override fun getString(file: String, keyEntry: String, defaultValue: String): String =
context.getSharedPreferences(file, Context.MODE_PRIVATE).getString(keyEntry, defaultValue) ?: ""

override fun getNullableString(file: String, keyEntry: String, defaultValue: String?): String? =
context.getSharedPreferences(file, Context.MODE_PRIVATE).getString(keyEntry, defaultValue)

override fun getLong(file: String, keyEntry: String, defaultValue: Long): Long =
context.getSharedPreferences(file, Context.MODE_PRIVATE).getLong(keyEntry, defaultValue)

override fun putBoolean(file: String, keyEntry: String, value: Boolean) {
context.getSharedPreferences(file, Context.MODE_PRIVATE)
.edit()
.putBoolean(keyEntry, value)
.apply()
}

override fun putString(file: String, keyEntry: String, value: String) {
override fun putString(file: String, keyEntry: String, value: String?) {
context.getSharedPreferences(file, Context.MODE_PRIVATE)
.edit()
.putString(keyEntry, value)
.apply()
}

override fun putLong(file: String, keyEntry: String, value: Long) {
context.getSharedPreferences(file, Context.MODE_PRIVATE)
.edit()
.putLong(keyEntry, value)
.apply()
}

override fun putInt(file: String, keyEntry: String, value: Int) {
context.getSharedPreferences(file, Context.MODE_PRIVATE)
.edit()
.putInt(keyEntry, value)
.apply()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ object SharedPrefConstants {
const val SDK_VERSION = "sdk_version"
const val CRYPTO_ENABLED = "should_encrypt"
const val CRYPTO_KEY_ALIAS = "crypto.key.alias"
const val CRYPTO_KEY_WRAPPER_ALIAS = "_crypto.key.wrapper.alias" // to store wrapper key of multi-user encryption key
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package apptentive.com.android.util

@InternalUseOnly
fun isAllNull(vararg args: Any?): Boolean {
return args.all { it == null }
}

@InternalUseOnly
fun isAnyNull(vararg args: Any?): Boolean {
return args.any { it == null }
}

@InternalUseOnly
fun isNoneNull(vararg args: Any?): Boolean {
return args.all { it != null }
}
Loading

0 comments on commit c9e1163

Please sign in to comment.