mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
feat: anonymous stat collection (2/2)
This commit is contained in:
+37
@@ -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 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 RMX1851" value="120" />
|
||||
<entry key="Tests" value="360" />
|
||||
<entry key="b36e6c44" value="120" />
|
||||
</map>
|
||||
</option>
|
||||
</AndroidTestResultsTableState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -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 })
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user