summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-08-11 20:23:49 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-08-11 20:23:49 +0000
commit30461398292773b9bd5862654da8fa31c0047a57 (patch)
tree0ac8809c30d78d17c98ed8eca7cde4c6266007b3
parent88dcb1636c4eb4ee603b28afab6a60080a544d6c (diff)
parentbe6946bab8641f9b105add75654d572c30dc0504 (diff)
downloadanalytics-library-30461398292773b9bd5862654da8fa31c0047a57.tar.gz
Snap for 8934313 from be6946bab8641f9b105add75654d572c30dc0504 to studio-ee-release
Change-Id: If2e56ef63ad042945a540a6b386d7fd0f9905ce8
-rw-r--r--shared/src/main/java/com/android/tools/analytics/AnalyticsSettings.kt109
-rw-r--r--shared/src/test/java/com/android/tools/analytics/AnalyticsSettingsTest.kt208
2 files changed, 205 insertions, 112 deletions
diff --git a/shared/src/main/java/com/android/tools/analytics/AnalyticsSettings.kt b/shared/src/main/java/com/android/tools/analytics/AnalyticsSettings.kt
index dc2b53d..1c0a151 100644
--- a/shared/src/main/java/com/android/tools/analytics/AnalyticsSettings.kt
+++ b/shared/src/main/java/com/android/tools/analytics/AnalyticsSettings.kt
@@ -21,9 +21,11 @@ import com.android.utils.ILogger
import com.google.common.annotations.VisibleForTesting
import com.google.common.base.Charsets
import com.google.common.io.Files
-import com.google.gson.GsonBuilder
import com.google.gson.JsonParseException
+import com.google.gson.TypeAdapter
import com.google.gson.annotations.SerializedName
+import com.google.gson.stream.JsonReader
+import com.google.gson.stream.JsonWriter
import java.io.File
import java.io.IOException
import java.io.InputStreamReader
@@ -36,11 +38,13 @@ import java.nio.channels.FileChannel
import java.nio.channels.OverlappingFileLockException
import java.nio.file.Paths
import java.security.SecureRandom
+import java.text.SimpleDateFormat
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
import java.time.temporal.ChronoUnit
import java.util.Date
+import java.util.Locale
import java.util.UUID
import java.util.concurrent.ScheduledExecutorService
import java.util.logging.Level
@@ -463,10 +467,7 @@ class AnalyticsSettingsData {
channel.truncate(0)
val outputStream = Channels.newOutputStream(channel)
val writer = OutputStreamWriter(outputStream)
-
- // Write out using pre-Java9 date format to let older releases read the file correctly.
- val datePatternJava8 = "MMM d, y h:mm:ss a"
- GsonBuilder().setDateFormat(datePatternJava8).create().toJson(this, writer)
+ DataTypeAdapter.write(JsonWriter(writer), this)
writer.flush()
outputStream.flush()
}
@@ -480,34 +481,16 @@ class AnalyticsSettingsData {
/**
* User id used for reporting analytics. This id is pseudo-anonymous.
*/
- @field:SerializedName("userId")
var userId: String? = null
-
- @field:SerializedName("hasOptedIn")
var optedIn: Boolean = false
-
- @field:SerializedName("debugDisablePublishing")
- val debugDisablePublishing: Boolean = false
-
- @field:SerializedName("saltValue")
+ var debugDisablePublishing: Boolean = false
+ private set
var saltValue = BigInteger.valueOf(0L)
-
- @field:SerializedName("saltSkew")
var saltSkew = AnalyticsSettings.SALT_SKEW_NOT_INITIALIZED
-
- @field:SerializedName("lastSentimentQuestionDate")
var lastSentimentQuestionDate: Date? = null
-
- @field:SerializedName("lastSentimentAnswerDate")
var lastSentimentAnswerDate: Date? = null
-
- @field:SerializedName("lastFeatureSurveyDate")
var nextFeatureSurveyDate: Date? = null
-
- @field:SerializedName("lastFeatureSurveyDateMap")
var nextFeatureSurveyDateMap: MutableMap<String, Date>? = null
-
- @field:SerializedName("lastOptinPromptVersion")
var lastOptinPromptVersion: String? = null
companion object {
@@ -517,10 +500,12 @@ class AnalyticsSettingsData {
file: File,
logger: ILogger? = null
): AnalyticsSettingsData? {
+ if (channel.size() == 0L) return null
val inputStream = Channels.newInputStream(channel)
- val gson = GsonBuilder().create()
return try {
- gson.fromJson(InputStreamReader(inputStream), AnalyticsSettingsData::class.java)
+ val reader = JsonReader(InputStreamReader(inputStream))
+ reader.isLenient = true
+ DataTypeAdapter.read(reader)
} catch (e: JsonParseException) {
logger?.warning("Unable to parse settings file %s: %s", file.toString(), e)
null
@@ -530,6 +515,76 @@ class AnalyticsSettingsData {
}
}
}
+
+ internal object DataTypeAdapter: TypeAdapter<AnalyticsSettingsData>() {
+
+ private val datePatternJava8 = SimpleDateFormat("MMM d, y h:mm:ss a", Locale.US)
+
+ override fun write(writer: JsonWriter, data: AnalyticsSettingsData) {
+ writer.beginObject()
+ data.userId?.let { writer.name("userId").value(it) }
+ writer.name("hasOptedIn").value(data.optedIn)
+ writer.name("debugDisablePublishing").value(data.debugDisablePublishing)
+ writer.name("saltValue").value(data.saltValue)
+ writer.name("saltSkew").value(data.saltSkew)
+ data.lastSentimentQuestionDate?.let { writer.name("lastSentimentQuestionDate").value(format(it)) }
+ data.lastSentimentAnswerDate?.let { writer.name("lastSentimentAnswerDate").value(format(it)) }
+ data.nextFeatureSurveyDate?.let { writer.name("lastFeatureSurveyDate").value(format(it)) }
+ data.nextFeatureSurveyDateMap?.let {
+ writer.name("lastFeatureSurveyDateMap")
+ writer.beginObject()
+ it.forEach { (key, value) ->
+ writer.name(key).value(format(value))
+ }
+ writer.endObject()
+ }
+ data.lastOptinPromptVersion?.let {
+ writer.name("lastOptinPromptVersion").value(it)
+ }
+ writer.endObject()
+ }
+
+ // Write out using pre-Java9 date format to let older releases read the file correctly.
+ private fun format(it: Date): String = datePatternJava8.format(it)
+
+ override fun read(reader: JsonReader): AnalyticsSettingsData {
+ val data = AnalyticsSettingsData()
+ if (!reader.hasNext()) {
+ return data
+ }
+ reader.beginObject()
+ while(reader.hasNext()) {
+ when (reader.nextName()) {
+ "userId" -> data.userId = reader.nextString()
+ "hasOptedIn" -> data.optedIn = reader.nextBoolean()
+ "debugDisablePublishing" -> data.debugDisablePublishing = reader.nextBoolean()
+ "saltValue" -> data.saltValue = BigInteger(reader.nextString())
+ "saltSkew" -> data.saltSkew = reader.nextInt()
+ "lastSentimentQuestionDate" -> data.lastSentimentQuestionDate = parseDate(reader.nextString())
+ "lastSentimentAnswerDate" -> data.lastSentimentAnswerDate = parseDate(reader.nextString())
+ "lastFeatureSurveyDate" -> data.nextFeatureSurveyDate = parseDate(reader.nextString())
+ "lastFeatureSurveyDateMap" -> {
+ val map = data.nextFeatureSurveyDateMap ?: mutableMapOf()
+ reader.beginObject()
+ while(reader.hasNext()) {
+ map[reader.nextName()] = parseDate(reader.nextString())
+ }
+ reader.endObject()
+ data.nextFeatureSurveyDateMap = map
+ }
+ "lastOptinPromptVersion" -> data.lastOptinPromptVersion = reader.nextString()
+ else -> reader.skipValue()
+ }
+ }
+ reader.endObject()
+ return data
+ }
+
+ private fun parseDate(string: String): Date {
+ return datePatternJava8.parse(string)
+ }
+ }
+
}
fun BigInteger.toByteArrayOfLength24(): ByteArray {
diff --git a/shared/src/test/java/com/android/tools/analytics/AnalyticsSettingsTest.kt b/shared/src/test/java/com/android/tools/analytics/AnalyticsSettingsTest.kt
index 372117b..5932dec 100644
--- a/shared/src/test/java/com/android/tools/analytics/AnalyticsSettingsTest.kt
+++ b/shared/src/test/java/com/android/tools/analytics/AnalyticsSettingsTest.kt
@@ -19,7 +19,6 @@ import com.android.tools.analytics.stubs.StubDateProvider
import com.android.utils.DateProvider
import com.android.utils.ILogger
import com.google.common.base.Charsets
-import com.google.gson.GsonBuilder
import com.google.protobuf.ByteString
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
@@ -34,11 +33,15 @@ import org.junit.rules.TemporaryFolder
import java.io.File
import java.io.IOException
import java.math.BigInteger
+import java.nio.channels.FileChannel
import java.nio.file.Files
-import java.nio.file.Paths
+import java.nio.file.Path
+import java.nio.file.StandardOpenOption
import java.util.Arrays
import java.util.Date
import java.util.UUID
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
/**
* Tests for [AnalyticsSettings].
@@ -50,19 +53,19 @@ class AnalyticsSettingsTest {
override fun error(
t: Throwable?, msgFormat: String?, vararg args: Any
) {
- fail()
+ fail("${msgFormat?.format(*args)} throwable=$t")
}
override fun warning(msgFormat: String, vararg args: Any) {
- fail()
+ fail(msgFormat.format(*args))
}
override fun info(msgFormat: String, vararg args: Any) {
- fail()
+ fail(msgFormat.format(*args))
}
override fun verbose(msgFormat: String, vararg args: Any) {
- fail()
+ fail(msgFormat.format(*args))
}
}
@@ -97,6 +100,13 @@ class AnalyticsSettingsTest {
@get:Rule
var thrown = ExpectedException.none()
+ private val analyticsSettingsFile: Path
+ get() = testConfigDir.root.toPath().resolve("analytics.settings")
+
+ private var analyticsSettingsFileContent: String
+ get() = analyticsSettingsFile.readText()
+ set(value) = analyticsSettingsFile.writeText(value)
+
@Test
@Throws(Exception::class)
fun loadExistingSettingsTest() {
@@ -104,11 +114,8 @@ class AnalyticsSettingsTest {
EnvironmentFakes.setCustomAndroidPrefsRootEnvironment(testConfigDir.root.toString())
try {
// Write a json settings file.
- val json = "{ userId: \"a4d47d92-8d4c-44bb-a8a4-d2483b6e0c16\", hasOptedIn: true }"
- Files.write(
- testConfigDir.root.toPath().resolve("analytics.settings"),
- json.toByteArray(Charsets.UTF_8)
- )
+ analyticsSettingsFileContent =
+ "{ userId: \"a4d47d92-8d4c-44bb-a8a4-d2483b6e0c16\", hasOptedIn: true }"
// read settings just written.
AnalyticsSettings.setInstanceForTest(null)
@@ -119,11 +126,8 @@ class AnalyticsSettingsTest {
assertTrue(AnalyticsSettings.optedIn)
// Write another json settings file
- val json2 = "{ userId: \"06120264-c9e7-492f-a39c-89c3cbee57c5\", hasOptedIn: false }"
- Files.write(
- testConfigDir.root.toPath().resolve("analytics.settings"),
- json2.toByteArray(Charsets.UTF_8)
- )
+ analyticsSettingsFileContent =
+ "{ userId: \"06120264-c9e7-492f-a39c-89c3cbee57c5\", hasOptedIn: false }"
// read settings just written.
AnalyticsSettings.setInstanceForTest(null)
AnalyticsSettings.initialize(failureLogger)
@@ -145,11 +149,7 @@ class AnalyticsSettingsTest {
)
try {
// Write non-valid json file content.
- val json = "BADFILE"
- Files.write(
- testConfigDir.root.toPath().resolve("analytics.settings"),
- json.toByteArray(Charsets.UTF_8)
- )
+ analyticsSettingsFileContent = "BADFILE"
AnalyticsSettings.setInstanceForTest(null)
AnalyticsSettings.initialize(countingLogger)
@@ -176,12 +176,8 @@ class AnalyticsSettingsTest {
)
try {
// Write non-valid json file content.
- val json =
+ analyticsSettingsFileContent =
"{\"hasOptedIn\":true,\"saltValue\":746227786052768374406922174584132630757738414714263142088,\"saltSkew\":632}"
- Files.write(
- testConfigDir.root.toPath().resolve("analytics.settings"),
- json.toByteArray(Charsets.UTF_8)
- )
AnalyticsSettings.setInstanceForTest(null)
AnalyticsSettings.initialize(failureLogger)
@@ -201,10 +197,7 @@ class AnalyticsSettingsTest {
)
try {
// Write empty file.
- Files.write(
- testConfigDir.root.toPath().resolve("analytics.settings"),
- byteArrayOf()
- )
+ analyticsSettingsFileContent = ""
AnalyticsSettings.setInstanceForTest(null)
AnalyticsSettings.initialize(failureLogger)
@@ -222,15 +215,8 @@ class AnalyticsSettingsTest {
EnvironmentFakes.setCustomAndroidPrefsRootEnvironment(
testConfigDir.root.toPath().toString()
)
- // The settings file should now be created.
- assertFalse(
- testConfigDir
- .root
- .toPath()
- .resolve("analytics.settings")
- .toFile()
- .exists()
- )
+ // The settings file should not be created.
+ assertFalse(Files.exists(analyticsSettingsFile))
try {
// load settings while there is no settings file present.
@@ -245,32 +231,22 @@ class AnalyticsSettingsTest {
assertFalse(AnalyticsSettings.optedIn)
// The settings file should now be created.
- assertTrue(
- testConfigDir
- .root
- .toPath()
- .resolve("analytics.settings")
- .toFile()
- .exists()
- )
-
- //AnalyticsSettings.saveSettings()
-
- // The settings file should still exist.
- assertTrue(
- testConfigDir
- .root
- .toPath()
- .resolve("analytics.settings")
- .toFile()
- .exists()
- )
+ assertTrue(analyticsSettingsFileContent.isNotEmpty())
// Reading the settings again should lead to the same data being read.
AnalyticsSettings.setInstanceForTest(null)
AnalyticsSettings.initialize(failureLogger)
assertFalse(AnalyticsSettings.optedIn)
assertEquals(uid, AnalyticsSettings.userId)
+ assertEquals(
+ """{"userId":"<uuid>","hasOptedIn":false,"debugDisablePublishing":false,"saltValue":0,"saltSkew":-1}""",
+ analyticsSettingsFileContent.normalizeUserid().normalizeSalt()
+ )
+ assertTrue(BigInteger(AnalyticsSettings.salt) != BigInteger.ZERO)
+ assertEquals(
+ """{"userId":"<uuid>","hasOptedIn":false,"debugDisablePublishing":false,"saltValue":<saltValue>,"saltSkew":<saltSkew>}""",
+ analyticsSettingsFileContent.normalizeUserid().normalizeSalt()
+ )
} finally {
EnvironmentFakes.setSystemEnvironment()
}
@@ -302,11 +278,22 @@ class AnalyticsSettingsTest {
// Default setting should be to not be opted in.
assertFalse(AnalyticsSettings.optedIn)
+ assertEquals(
+ """{"userId":"db3dd15b-053a-4066-ac93-04c50585edc2","hasOptedIn":false,"debugDisablePublishing":false,"saltValue":0,"saltSkew":-1}""",
+ analyticsSettingsFileContent.normalizeSalt()
+ )
+ assertTrue(BigInteger(AnalyticsSettings.salt) != BigInteger.ZERO)
+ // Getting the salt should trigger the file to be updated with the new salt value
+ assertEquals(
+ """{"userId":"db3dd15b-053a-4066-ac93-04c50585edc2","hasOptedIn":false,"debugDisablePublishing":false,"saltValue":<saltValue>,"saltSkew":<saltSkew>}""",
+ analyticsSettingsFileContent.normalizeSalt()
+ )
} finally {
EnvironmentFakes.setSystemEnvironment()
}
}
+
@Test
@Throws(Exception::class)
fun changeSettingsTest() {
@@ -317,11 +304,8 @@ class AnalyticsSettingsTest {
try {
// Start with an existing config on disk.
- val json = "{ userId: \"a4d47d92-8d4c-44bb-a8a4-d2483b6e0c16\", hasOptedIn: true }"
- Files.write(
- testConfigDir.root.toPath().resolve("analytics.settings"),
- json.toByteArray(Charsets.UTF_8)
- )
+ analyticsSettingsFileContent =
+ "{ userId: \"a4d47d92-8d4c-44bb-a8a4-d2483b6e0c16\", hasOptedIn: true }"
AnalyticsSettings.setInstanceForTest(null)
AnalyticsSettings.initialize(failureLogger)
@@ -465,6 +449,10 @@ class AnalyticsSettingsTest {
AnalyticsSettings.optedIn = true
// Write updated settings to disk
AnalyticsSettings.saveSettings()
+ assertEquals(
+ """{"userId":"<uuid>","hasOptedIn":true,"debugDisablePublishing":false,"saltValue":0,"saltSkew":-1}""",
+ analyticsSettingsFileContent.normalizeUserid().normalizeSalt()
+ )
// Read settings and verify that changes have persisted.
AnalyticsSettings.setInstanceForTest(null)
@@ -504,11 +492,7 @@ class AnalyticsSettingsTest {
try {
// Write a json settings file.
val json = "{ userId: \"a4d47d92-8d4c-44bb-a8a4-d2483b6e0c16\", hasOptedIn: true }"
- Files.write(
- testConfigDir.root.toPath().resolve("analytics.settings"),
- json.toByteArray(Charsets.UTF_8)
- )
-
+ analyticsSettingsFileContent = json
// ensure the settings are not initialized
AnalyticsSettings.setInstanceForTest(null)
// disable analytics
@@ -517,6 +501,10 @@ class AnalyticsSettingsTest {
assertTrue(AnalyticsSettings.initialized)
assertFalse(AnalyticsSettings.optedIn)
assertEquals("", AnalyticsSettings.userId)
+ assertEquals(
+ json,
+ analyticsSettingsFileContent
+ ) // The analytics settings file is not touched
} finally {
EnvironmentFakes.setSystemEnvironment()
}
@@ -532,24 +520,74 @@ class AnalyticsSettingsTest {
lastSentimentAnswerDate = Date(115, 4, 17, 14, 23, 45)
})
AnalyticsSettings.saveSettings()
- val analysticsSettingsContents =
- String(
- Files.readAllBytes(
- Paths.get(
- testConfigDir.root.toString(),
- "analytics.settings"
- )
- ), Charsets.UTF_8
- )
- val lastSentimentAnswerDate =
- GsonBuilder().create()
- .fromJson(
- analysticsSettingsContents,
- Map::class.java
- )["lastSentimentAnswerDate"]
- assertEquals("May 17, 2015 2:23:45 PM", lastSentimentAnswerDate)
+
+ assertEquals(
+ """{"userId":"<uuid>","hasOptedIn":true,"debugDisablePublishing":false,"saltValue":0,"saltSkew":-1,"lastSentimentAnswerDate":"May 17, 2015 2:23:45 PM"}""",
+ analyticsSettingsFileContent.normalizeUserid().normalizeSalt()
+ )
} finally {
EnvironmentFakes.setSystemEnvironment()
}
}
+
+ @Test
+ fun settingsDataIncludesAllFields() {
+ EnvironmentFakes.setCustomAndroidPrefsRootEnvironment(testConfigDir.root.toString())
+ try {
+ val allFieldsSettingsContent =
+ """{"userId":"db3dd15b-053a-4066-ac93-04c50585edc2","hasOptedIn":true,"debugDisablePublishing":true,"saltValue":1234,"saltSkew":567,"lastSentimentQuestionDate":"May 17, 2015 2:23:45 PM","lastSentimentAnswerDate":"May 18, 2015 2:23:45 PM","lastFeatureSurveyDate":"May 19, 2015 2:23:45 PM","lastFeatureSurveyDateMap":{"survey1":"May 20, 2015 2:23:45 PM"},"lastOptinPromptVersion":"2020.3.4"}"""
+
+ analyticsSettingsFileContent = allFieldsSettingsContent
+
+ val settingsData =
+ FileChannel.open(
+ analyticsSettingsFile,
+ StandardOpenOption.READ,
+ StandardOpenOption.WRITE
+ ).use { channel ->
+ AnalyticsSettingsData.parseSettingsData(
+ channel,
+ analyticsSettingsFile.toFile(),
+ failureLogger
+ )!!
+ }
+ assertEquals("db3dd15b-053a-4066-ac93-04c50585edc2", settingsData.userId)
+ assertEquals(true, settingsData.optedIn)
+ assertEquals(true, settingsData.debugDisablePublishing)
+ assertEquals(BigInteger.valueOf(1234), settingsData.saltValue)
+ assertEquals(567, settingsData.saltSkew)
+ assertEquals(Date(115, 4, 17, 14, 23, 45), settingsData.lastSentimentQuestionDate)
+ assertEquals(Date(115, 4, 18, 14, 23, 45), settingsData.lastSentimentAnswerDate)
+ assertEquals(Date(115, 4, 19, 14, 23, 45), settingsData.nextFeatureSurveyDate)
+ assertEquals(
+ mapOf("survey1" to Date(115, 4, 20, 14, 23, 45)),
+ settingsData.nextFeatureSurveyDateMap
+ )
+ assertEquals("2020.3.4", settingsData.lastOptinPromptVersion)
+
+
+ settingsData.saveSettings(failureLogger)
+
+ assertEquals(allFieldsSettingsContent, analyticsSettingsFileContent)
+ } finally {
+ EnvironmentFakes.setSystemEnvironment()
+ }
+ }
+
+ private fun String.normalizeUserid(): String {
+ return this.replace(
+ regex = Regex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"),
+ replacement = "<uuid>"
+ )
+ }
+
+ private fun String.normalizeSalt(): String {
+ return this.replace(
+ regex = Regex(""""saltValue":-?\d{2,}"""),
+ replacement = "\"saltValue\":<saltValue>"
+ ).replace(
+ regex = Regex(""""saltSkew":\d{2,}"""),
+ replacement = "\"saltSkew\":<saltSkew>"
+ )
+ }
}