feat: anonymous stat collection (2/2)

This commit is contained in:
Akash Yadav
2023-06-21 16:35:33 +05:30
parent e45dfc146b
commit af6f45ea85
18 changed files with 536 additions and 15 deletions
+37
View File
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidTestResultsUserPreferences">
<option name="androidTestResultsTableState">
<map>
<entry key="-985524882">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Realme&#10; RMX1851" value="120" />
<entry key="Tests" value="360" />
<entry key="b36e6c44" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-528694390">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Realme&#10; RMX1851" value="120" />
<entry key="Tests" value="360" />
<entry key="b36e6c44" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
</map>
</option>
</component>
</project>
+2
View File
@@ -114,6 +114,8 @@ dependencies {
implementation(libs.androidx.transition)
implementation(libs.androidx.vectors)
implementation(libs.androidx.animated.vectors)
implementation(libs.androidx.work)
implementation(libs.androidx.work.ktx)
implementation(libs.google.material)
implementation(libs.google.flexbox)
implementation(libs.google.oss.licenses)
@@ -40,6 +40,8 @@ import com.itsaky.androidide.preferences.internal.NO_OPENED_PROJECT
import com.itsaky.androidide.preferences.internal.autoOpenProjects
import com.itsaky.androidide.preferences.internal.confirmProjectOpen
import com.itsaky.androidide.preferences.internal.lastOpenedProject
import com.itsaky.androidide.preferences.internal.statConsentDialogShown
import com.itsaky.androidide.preferences.internal.statOptIn
import com.itsaky.androidide.projects.ProjectManager.projectPath
import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.templates.ITemplateProvider
@@ -91,10 +93,15 @@ class MainActivity : LimitlessIDEActivity() {
return
}
if (!checkToolsIsInstalled()) {
showDialogInstallJdkSdk()
} else {
openLastProject()
showStatConsentDialogIfNeeded {
app.reportStatsIfNecessary()
if (!checkToolsIsInstalled()) {
showDialogInstallJdkSdk()
} else {
openLastProject()
}
}
viewModel.currentScreen.observe(this) { screen ->
@@ -121,6 +128,32 @@ class MainActivity : LimitlessIDEActivity() {
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
private fun showStatConsentDialogIfNeeded(onResult: () -> Unit) {
if (statConsentDialogShown) {
onResult()
return
}
DialogUtils.newMaterialDialogBuilder(this).apply {
setTitle(string.title_androidide_statistics)
setMessage(string.msg_androidide_statistics)
setCancelable(false)
setPositiveButton(string.btn_opt_in) { dialog, _ ->
dialog.dismiss()
statOptIn = true
}
setNegativeButton(string.btn_no_thanks) { dialog, _ ->
dialog.dismiss()
statOptIn = false
}
setOnDismissListener {
statConsentDialogShown = true
onResult()
}
show()
}
}
override fun onInsetsUpdated(insets: Rect) {
super.onInsetsUpdated(insets)
binding.fragmentContainersParent.setPadding(insets.left, 0, insets.right, insets.bottom)
@@ -146,7 +179,7 @@ class MainActivity : LimitlessIDEActivity() {
// template details -> template list
val setAxisToX =
(previous == SCREEN_TEMPLATE_LIST || previous == SCREEN_TEMPLATE_DETAILS) &&
(screen == SCREEN_TEMPLATE_LIST || screen == SCREEN_TEMPLATE_DETAILS)
(screen == SCREEN_TEMPLATE_LIST || screen == SCREEN_TEMPLATE_DETAILS)
val axis = if (setAxisToX) {
MaterialSharedAxis.X
@@ -21,19 +21,31 @@ package com.itsaky.androidide.app
import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.Observer
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.Operation
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.blankj.utilcode.util.ThrowableUtils.getFullStackTrace
import com.google.android.material.color.DynamicColors
import com.itsaky.androidide.BuildConfig
import com.itsaky.androidide.activities.CrashHandlerActivity
import com.itsaky.androidide.buildinfo.BuildInfo
import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider
import com.itsaky.androidide.eventbus.events.preferences.PreferenceChangeEvent
import com.itsaky.androidide.events.AppEventsIndex
import com.itsaky.androidide.events.EditorEventsIndex
import com.itsaky.androidide.events.LspApiEventsIndex
import com.itsaky.androidide.events.LspJavaEventsIndex
import com.itsaky.androidide.events.ProjectsApiEventsIndex
import com.itsaky.androidide.preferences.internal.STAT_OPT_IN
import com.itsaky.androidide.preferences.internal.enableMaterialYou
import com.itsaky.androidide.preferences.internal.statOptIn
import com.itsaky.androidide.preferences.internal.uiMode
import com.itsaky.androidide.stats.AndroidIDEStats
import com.itsaky.androidide.stats.StatUploadWorker
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE
import com.itsaky.androidide.tasks.executeAsync
import com.itsaky.androidide.treesitter.TreeSitter
@@ -42,12 +54,16 @@ import com.itsaky.androidide.utils.VMUtils
import com.itsaky.androidide.utils.flashError
import io.github.rosemoe.sora.widget.schemes.EditorColorScheme
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.lang.Thread.UncaughtExceptionHandler
import java.time.Duration
import kotlin.system.exitProcess
class IDEApplication : BaseApplication() {
private var uncaughtExceptionHandler: UncaughtExceptionHandler? = null
private val log = ILogger.newInstance("IDEApplication")
init {
if (!VMUtils.isJvm()) {
@@ -70,6 +86,8 @@ class IDEApplication : BaseApplication() {
.addIndex(LspJavaEventsIndex())
.installDefaultEventBus(true)
EventBus.getDefault().register(this)
AppCompatDelegate.setDefaultNightMode(uiMode)
if (enableMaterialYou) {
@@ -117,6 +135,59 @@ class IDEApplication : BaseApplication() {
}
}
fun reportStatsIfNecessary() {
if (!statOptIn) {
log.info("Stat collection is disabled.")
return
}
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
val request = PeriodicWorkRequestBuilder<StatUploadWorker>(Duration.ofHours(24))
.setInputData(AndroidIDEStats.statData.toInputData())
.setConstraints(constraints)
.addTag(StatUploadWorker.WORKER_WORK_NAME)
.build()
val workManager = WorkManager.getInstance(this)
log.info("reportStatsIfNecessary: Enqueuing StatUploadWorker...")
val operation = workManager
.enqueueUniquePeriodicWork(StatUploadWorker.WORKER_WORK_NAME,
ExistingPeriodicWorkPolicy.UPDATE, request)
operation.state.observeForever(object : Observer<Operation.State> {
override fun onChanged(t: Operation.State?) {
operation.state.removeObserver(this)
log.debug("reportStatsIfNecessary: WorkManager enqueue result: $t")
}
})
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onPrefChanged(event: PreferenceChangeEvent) {
if (event.key == STAT_OPT_IN) {
val enabled = event.value as Boolean
if (enabled) {
reportStatsIfNecessary()
} else {
cancelStatUploadWorker()
}
}
}
private fun cancelStatUploadWorker() {
log.info("Opted-out of stat collection. Cancelling StatUploadWorker if enqueued...")
val operation = WorkManager.getInstance(this)
.cancelUniqueWork(StatUploadWorker.WORKER_WORK_NAME)
operation.state.observeForever(object : Observer<Operation.State> {
override fun onChanged(t: Operation.State?) {
operation.state.removeObserver(this)
log.info("StatUploadWorker: Cancellation result state: $t")
}
})
}
companion object {
private val LOG = ILogger.newInstance("IDEApplication")
@@ -23,6 +23,7 @@ import com.itsaky.androidide.preferences.internal.STAT_OPT_IN
import com.itsaky.androidide.preferences.internal.statOptIn
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.stats.AndroidIDEStats
import com.itsaky.androidide.stats.StatUploadWorker
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -80,36 +81,36 @@ private abstract class StatDataPreference(
@Parcelize
private class UniqueID(
override val key: String = "device_hash",
override val key: String = StatUploadWorker.KEY_DEVICE_ID,
override val title: Int = R.string.title_unique_id,
) : StatDataPreference({ AndroidIDEStats.uniqueDeviceId })
@Parcelize
private class DeviceName(
override val key: String = "device_name",
override val key: String = StatUploadWorker.KEY_DEVICE_NAME,
override val title: Int = R.string.title_device
) : StatDataPreference({ AndroidIDEStats.deviceModel })
@Parcelize
private class AndroidVersion(
override val key: String = "android_version",
override val key: String = StatUploadWorker.KEY_ANDROID_VERSION,
override val title: Int = R.string.title_android_veresion
) : StatDataPreference({ AndroidIDEStats.androidVersion.toString() })
@Parcelize
private class AppVersion(
override val key: String = "app_version",
override val key: String = StatUploadWorker.KEY_APP_VERSION,
override val title: Int = R.string.title_app_version
) : StatDataPreference({ AndroidIDEStats.appVersion })
@Parcelize
private class Country(
override val key: String = "device_hash",
override val key: String = StatUploadWorker.KEY_DEVICE_COUNTRY,
override val title: Int = R.string.title_country
) : StatDataPreference({ AndroidIDEStats.country })
@Parcelize
private class CpuArch(
override val key: String = "app_cpu_arch",
override val key: String = StatUploadWorker.KEY_APP_CPU_ARCH,
override val title: Int = R.string.title_cpu_arch
) : StatDataPreference({ AndroidIDEStats.cpuArch })
+2
View File
@@ -56,6 +56,8 @@ fun Project.configureBaseExtension() {
targetSdk = BuildConfig.targetSdk
versionCode = projectVersionCode
versionName = rootProject.version.toString()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
@@ -19,10 +19,8 @@ package com.itsaky.androidide.managers;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import com.itsaky.androidide.eventbus.events.preferences.PreferenceChangeEvent;
import com.itsaky.androidide.eventbus.events.preferences.PreferenceRemoveEvent;
import org.greenrobot.eventbus.EventBus;
public class PreferenceManager {
@@ -99,6 +97,15 @@ public class PreferenceManager {
dispatchChangeEvent(key, value);
}
public long getLong(String key, long defaultValue) {
return prefs.getLong(key, defaultValue);
}
public void putLong(String key, long value) {
prefs.edit().putLong(key, value).apply();
dispatchChangeEvent(key, value);
}
public int getInt(String key, int def) {
return prefs.getInt(key, def);
}
+9
View File
@@ -10,6 +10,8 @@ androidx-vectordrawable = "1.1.0"
androidx-navigation = "2.5.3"
ksp = "1.8.21-1.0.11"
antlr4 = "4.13.0"
androidx-work = "2.8.1"
retrofit = "2.9.0"
[libraries]
@@ -41,6 +43,8 @@ common-javapoet = { module = "com.squareup:javapoet", version = "1.13.0" }
common-ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
common-jsonrpc = { module = "org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc", version = "0.20.1" }
common-leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version = "2.11"}
common-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
common-retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
# AndroidX
androidx-annotation = { module = "androidx.annotation:annotation", version = "1.6.0" }
@@ -62,6 +66,8 @@ androidx-transition = { module = "androidx.transition:transition-ktx", version =
androidx-nav-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" }
androidx-nav-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" }
androidx-nav-dynamic_features = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "androidx-navigation" }
androidx-work = { module = "androidx.work:work-runtime", version.ref = "androidx-work" }
androidx-work-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" }
# Google
google-material = { module = "com.google.android.material:material", version = "1.9.0" }
@@ -98,7 +104,10 @@ tests-junit = { module = "junit:junit", version = "4.13.2" }
tests-google-truth = { module = "com.google.truth:truth", version = "1.1.3" }
tests-robolectric = { module = "org.robolectric:robolectric", version = "4.10.3" }
tests-androidx-test-core = { module = "androidx.test:core", version = "1.5.0" }
tests-androidx-test-runner = { module = "androidx.test:runner", version = "1.5.2" }
tests-androidx-test-rules = { module = "androidx.test:rules", version = "1.5.0" }
tests-androidx-junit = { module = "androidx.test.ext:junit", version = "1.1.5" }
tests-androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "androidx-work" }
tests-androidx-espresso = { module = "androidx.test.espresso:espresso-core", version = "3.5.1" }
tests-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "5.0.0" }
tests-mockk = { module = "io.mockk:mockk", version = "1.13.5" }
+16
View File
@@ -25,8 +25,24 @@ android {
}
dependencies {
implementation(libs.androidx.work)
implementation(libs.androidx.work.ktx)
implementation(libs.common.retrofit)
implementation(libs.common.retrofit.gson)
implementation(libs.common.utilcode)
implementation(libs.google.gson)
implementation(projects.buildInfo)
implementation(projects.common)
implementation(projects.logger)
implementation(projects.preferences)
androidTestImplementation(libs.tests.androidx.junit)
androidTestImplementation(libs.tests.androidx.test.core)
androidTestImplementation(libs.tests.androidx.test.runner)
androidTestImplementation(libs.tests.androidx.test.rules)
androidTestImplementation(libs.tests.androidx.work.testing)
androidTestImplementation(libs.androidx.work)
androidTestImplementation(libs.androidx.work.ktx)
androidTestImplementation(libs.tests.google.truth)
}
@@ -0,0 +1,80 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.stats
import android.app.Application
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.work.Configuration
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.impl.utils.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.buildinfo.BuildInfo
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeUnit
/**
* @author Akash Yadav
*/
@RunWith(AndroidJUnit4::class)
class StatUploadWorkerTest {
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Application>()
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.build()
// Initialize WorkManager for instrumentation tests.
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
@Test
@Throws(Exception::class)
fun testStatUploadWorker() {
val context = ApplicationProvider.getApplicationContext<Application>()
val input = StatData("test_device_id", "test_device_name", "test_device_country", 33,
BuildInfo.VERSION_NAME_SIMPLE, "aarch64")
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
val request = PeriodicWorkRequestBuilder<StatUploadWorker>(24, TimeUnit.HOURS)
.setInputData(input.toInputData())
.setConstraints(constraints)
.build()
val workManager = WorkManager.getInstance(context)
val testDriver = WorkManagerTestInitHelper.getTestDriver(context)!!
workManager.enqueueUniquePeriodicWork(StatUploadWorker.WORKER_WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP, request).result.get()
testDriver.setAllConstraintsMet(request.id)
testDriver.setPeriodDelayMet(request.id)
val workInfo = workManager.getWorkInfoById(request.id).get()
assertThat(workInfo.state).isEqualTo(WorkInfo.State.ENQUEUED)
}
}
+1 -1
View File
@@ -17,5 +17,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -61,6 +61,10 @@ object AndroidIDEStats {
BaseApplication.getArch()!!
}
val statData by lazy {
StatData(uniqueDeviceId, deviceModel, country, androidVersion, appVersion, cpuArch)
}
private fun digest(input: String): String {
return try {
val md = MessageDigest.getInstance("SHA-256")
@@ -0,0 +1,100 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.stats
import android.os.Build
import androidx.work.Data
import androidx.work.workDataOf
import com.google.gson.annotations.SerializedName
import com.itsaky.androidide.stats.StatUploadWorker.Companion.KEY_ANDROID_VERSION
import com.itsaky.androidide.stats.StatUploadWorker.Companion.KEY_APP_CPU_ARCH
import com.itsaky.androidide.stats.StatUploadWorker.Companion.KEY_APP_VERSION
import com.itsaky.androidide.stats.StatUploadWorker.Companion.KEY_DEVICE_COUNTRY
import com.itsaky.androidide.stats.StatUploadWorker.Companion.KEY_DEVICE_ID
import com.itsaky.androidide.stats.StatUploadWorker.Companion.KEY_DEVICE_NAME
/**
* Data that is uploaded to the server.
*
* @author Akash Yadav
*/
data class StatData(
@SerializedName(KEY_DEVICE_ID)
val deviceId: String,
@SerializedName(KEY_DEVICE_NAME)
val deviceName: String,
@SerializedName(KEY_DEVICE_COUNTRY)
val deviceCountry: String,
@SerializedName(KEY_ANDROID_VERSION)
val androidVersion: Int,
@SerializedName(KEY_APP_VERSION)
val appVersion: String,
@SerializedName(KEY_APP_CPU_ARCH)
val cpuArch: String
) {
companion object {
/**
* Creates the [StatData] from the given [input data][Data].
*
* @param inputData The input data.
*/
@JvmStatic
fun fromInputData(inputData: Data): StatData {
val deviceId = inputData.getString(KEY_DEVICE_ID)
val deviceName = inputData.getString(KEY_DEVICE_NAME)
val deviceCountry = inputData.getString(KEY_DEVICE_COUNTRY)
val androidVersion = inputData.getInt(KEY_ANDROID_VERSION, 0)
val appVersion = inputData.getString(KEY_APP_VERSION)
val cpuArch = inputData.getString(KEY_APP_CPU_ARCH)
if (deviceId == null
|| deviceName == null
|| deviceCountry == null
|| androidVersion < Build.VERSION_CODES.O
|| appVersion == null
|| cpuArch == null
) {
throw IllegalArgumentException(
"Work data does not contain required fields or has invalid field values")
}
return StatData(deviceId, deviceName, deviceCountry, androidVersion, appVersion, cpuArch)
}
}
/**
* Get the stat data as [input data][Data].
*/
fun toInputData(): Data {
return workDataOf(
KEY_DEVICE_ID to deviceId,
KEY_DEVICE_NAME to deviceName,
KEY_DEVICE_COUNTRY to deviceCountry,
KEY_ANDROID_VERSION to androidVersion,
KEY_APP_VERSION to appVersion,
KEY_APP_CPU_ARCH to cpuArch
)
}
}
@@ -0,0 +1,33 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.stats
import com.google.gson.annotations.SerializedName
/**
* Response received from the server.
*
* @author Akash Yadav
*/
internal data class StatUploadResponse(
@SerializedName("status")
val status: Int,
@SerializedName("message")
val message: String
)
@@ -0,0 +1,33 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.stats
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
/**
* Retrofit service for stat upload.
*
* @author Akash Yadav
*/
internal interface StatUploadService {
@POST("/stats/")
fun uploadStats(@Body data: StatData): Call<StatUploadResponse>
}
@@ -0,0 +1,83 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.stats
import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.itsaky.androidide.buildinfo.BuildInfo
import com.itsaky.androidide.utils.ILogger
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
/**
* Worker to upload stats to the server.
*
* @author Akash Yadav
*/
class StatUploadWorker(context: Context, workerParams: WorkerParameters) : Worker(context,
workerParams) {
companion object {
const val WORKER_WORK_NAME = "ide.stats.uploadWorker"
const val KEY_DEVICE_ID = "device_hash"
const val KEY_DEVICE_NAME = "device_name"
const val KEY_DEVICE_COUNTRY = "device_country"
const val KEY_ANDROID_VERSION = "android_version"
const val KEY_APP_VERSION = "app_version"
const val KEY_APP_CPU_ARCH = "app_cpu_arch"
private val log = ILogger.newInstance("StatUploadWorker")
}
override fun doWork(): Result {
val data = StatData.fromInputData(inputData = inputData)
log.debug("Uploading stats: $data")
val retrofit = Retrofit.Builder().baseUrl(BuildInfo.PROJECT_SITE)
.addConverterFactory(GsonConverterFactory.create()).build()
val service = retrofit.create(StatUploadService::class.java)
val response = try {
service.uploadStats(data).execute()
} catch (err: Exception) {
log.error("Failed to upload stats to server", err)
return Result.retry()
}
val body = response.body()
if (!response.isSuccessful || body == null) {
log.error(
"Stat upload failed: responseCode: ${response.code()}, responseBody: $body, errBody: ${
response.errorBody()?.string() ?: "(empty)"
}")
// try again next time
return Result.failure()
}
if (body.status != 200) {
log.error("Stat upload failed: response: $body")
return Result.failure()
}
log.info("Stat upload successful: $body")
return Result.success()
}
}
@@ -19,8 +19,9 @@ package com.itsaky.androidide.preferences.internal
const val STAT_COLLECTION_CONSENT_SHOWN = "ide.stats.consentShown"
const val STAT_OPT_IN = "ide.stats.optIn"
const val STAT_LAST_REPORTED = "ide.stats.lastReported"
var statConsentShown: Boolean
var statConsentDialogShown: Boolean
get() = prefManager.getBoolean(STAT_COLLECTION_CONSENT_SHOWN, false)
set(value) {
prefManager.putBoolean(STAT_COLLECTION_CONSENT_SHOWN, value)
@@ -30,4 +31,10 @@ var statOptIn: Boolean
get() = prefManager.getBoolean(STAT_OPT_IN, false)
set(value) {
prefManager.putBoolean(STAT_OPT_IN, value)
}
var statLastReported: Long
get() = prefManager.getLong(STAT_LAST_REPORTED, 0L)
set(value) {
prefManager.putLong(STAT_LAST_REPORTED, value)
}
@@ -568,6 +568,7 @@
<string name="msg_use_kts">Use Kotlin Script (.kts) for Gradle build files</string>
<string name="title_privacy">Privacy</string>
<string name="title_androidide_statistics">AndroidIDE Statistics</string>
<string name="msg_androidide_statistics">Help us improve AndroidIDE by providing anonymous statistics. We collect basic information about your device for user base statistics only. The exact data that is shared can be viewed in preferences. Your data is secure and not shared with third-party services. You can opt-out anytime in preferences.</string>
<string name="summary_anonymous_usage_stats">Anonymous usage stats</string>
<string name="idepref_stats_optIn_title">Stats collection</string>
<string name="idepref_stats_optIn_summary">Allow installation metrics and device statistics to be collected (fully anonymous).</string>
@@ -578,4 +579,6 @@
<string name="title_cpu_arch">CPU Arch</string>
<string name="title_preview_data">Preview data</string>
<string name="title_android_veresion">Android veresion</string>
<string name="btn_opt_in">Opt-in</string>
<string name="btn_no_thanks">No, thanks</string>
</resources>